@chainvue/verus-sapling 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +74 -0
- package/README.md +194 -0
- package/crate/pkg/package.json +17 -0
- package/crate/pkg/verus_sapling_prover.d.ts +71 -0
- package/crate/pkg/verus_sapling_prover.js +451 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm +0 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm.d.ts +15 -0
- package/dist/browser/grpcweb.d.ts +29 -0
- package/dist/browser/grpcweb.js +149 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +23 -0
- package/dist/browser/lightwalletd-web.d.ts +36 -0
- package/dist/browser/lightwalletd-web.js +186 -0
- package/dist/browser/protobuf.d.ts +38 -0
- package/dist/browser/protobuf.js +115 -0
- package/dist/browser/prover-worker.d.ts +18 -0
- package/dist/browser/prover-worker.js +61 -0
- package/dist/browser/worker-prover.d.ts +38 -0
- package/dist/browser/worker-prover.js +51 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +25 -0
- package/dist/hex.d.ts +11 -0
- package/dist/hex.js +32 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +35 -0
- package/dist/lightwalletd.d.ts +113 -0
- package/dist/lightwalletd.js +99 -0
- package/dist/money.d.ts +23 -0
- package/dist/money.js +40 -0
- package/dist/parse.d.ts +45 -0
- package/dist/parse.js +129 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +13 -0
- package/dist/wallet.d.ts +124 -0
- package/dist/wallet.js +179 -0
- package/dist/wasm.d.ts +91 -0
- package/dist/wasm.js +90 -0
- package/dist/zaddr.d.ts +19 -0
- package/dist/zaddr.js +107 -0
- package/package.json +90 -0
- package/proto/compact_formats.proto +62 -0
- package/proto/darkside.proto +124 -0
- package/proto/service.proto +172 -0
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shielded wallet orchestration — the typed home for the flows proven end to end
|
|
3
|
+
* on Verus testnet, sourcing ALL chain data from lightwalletd (no full node):
|
|
4
|
+
*
|
|
5
|
+
* - `detectNotes` — find the wallet's own notes by trial-decrypting the
|
|
6
|
+
* compact blocks of a height range, and drop any already
|
|
7
|
+
* spent within that range. Replaces `z_listunspent`.
|
|
8
|
+
* - `buildShieldedSpend`— assemble and prove a z→z / z→t spend of one detected
|
|
9
|
+
* note (fetches the note's creating output, tree state,
|
|
10
|
+
* and block cmus, then calls the prover).
|
|
11
|
+
*
|
|
12
|
+
* This module is transport- and prover-agnostic: it depends only on the
|
|
13
|
+
* `LightwalletdTransport` interface and a prover callback. That keeps the money
|
|
14
|
+
* invariant and marshalling here (bigint sats, checked crossings) while letting
|
|
15
|
+
* the caller decide where the wasm runs — directly in Node, or in a Web Worker
|
|
16
|
+
* in the browser (spend proving is ~5–20 s; detection is milliseconds).
|
|
17
|
+
*
|
|
18
|
+
* Proven reference: the fully-lightwalletd-sourced z→z (txid 07e3b38e…f996) and
|
|
19
|
+
* the note-detection cross-check whose predicted nullifier matched that spend's
|
|
20
|
+
* on-chain nullifier byte-for-byte.
|
|
21
|
+
*/
|
|
22
|
+
import { ShieldedInputError } from './errors.js';
|
|
23
|
+
import { bytesToHex, reverseBytes } from './hex.js';
|
|
24
|
+
import { CONSENSUS_BRANCH_ID, toSafeNumber } from './money.js';
|
|
25
|
+
/** Enforce the money invariant at the boundary: a `bigint`, and >= 0. */
|
|
26
|
+
function assertSats(label, sats) {
|
|
27
|
+
if (typeof sats !== 'bigint') {
|
|
28
|
+
throw new ShieldedInputError(`${label} must be a bigint (satoshis), got ${typeof sats}`);
|
|
29
|
+
}
|
|
30
|
+
if (sats < 0n)
|
|
31
|
+
throw new ShieldedInputError(`${label} must be >= 0, got ${sats}`);
|
|
32
|
+
}
|
|
33
|
+
import { parseSaplingOutput, parseTreeState } from './parse.js';
|
|
34
|
+
import { saplingAddressToHex } from './zaddr.js';
|
|
35
|
+
const toHex = (b) => bytesToHex(b);
|
|
36
|
+
/**
|
|
37
|
+
* Find the wallet's own notes in `[fromHeight, toHeight]` by trial-decryption,
|
|
38
|
+
* excluding notes whose nullifier is spent within the same range.
|
|
39
|
+
*
|
|
40
|
+
* Sources everything from `transport`: the tree state before the range (for
|
|
41
|
+
* absolute positions) plus every compact block in the range. `prover` is the
|
|
42
|
+
* wasm `detectNotes` (cheap; may run on the main thread).
|
|
43
|
+
*
|
|
44
|
+
* Note: spends are only observed within the scanned range. Scan from the wallet
|
|
45
|
+
* birthday to the tip for a complete unspent set.
|
|
46
|
+
*/
|
|
47
|
+
export async function detectNotes(transport, prover, params) {
|
|
48
|
+
const toHeight = params.toHeight ?? (await transport.getLatestHeight());
|
|
49
|
+
if (toHeight < params.fromHeight)
|
|
50
|
+
return [];
|
|
51
|
+
const treeState = await transport.getTreeState(params.fromHeight - 1);
|
|
52
|
+
const tree = parseTreeState(treeState.tree);
|
|
53
|
+
const outputs = [];
|
|
54
|
+
const spentNullifiers = new Set();
|
|
55
|
+
const txidByKey = new Map(); // `${height}:${tx_index}` -> display txid
|
|
56
|
+
for await (const block of transport.getBlockRange(params.fromHeight, toHeight)) {
|
|
57
|
+
const height = Number(block.height);
|
|
58
|
+
for (const tx of block.vtx) {
|
|
59
|
+
const txIndex = Number(tx.index);
|
|
60
|
+
txidByKey.set(`${height}:${txIndex}`, bytesToHex(reverseBytes(tx.hash)));
|
|
61
|
+
for (const s of tx.spends)
|
|
62
|
+
spentNullifiers.add(toHex(s.nf));
|
|
63
|
+
tx.outputs.forEach((o, outputIndex) => {
|
|
64
|
+
outputs.push({
|
|
65
|
+
height,
|
|
66
|
+
tx_index: txIndex,
|
|
67
|
+
output_index: outputIndex,
|
|
68
|
+
cmu: toHex(o.cmu),
|
|
69
|
+
epk: toHex(o.epk),
|
|
70
|
+
ciphertext: toHex(o.ciphertext),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const keyField = 'extskHex' in params.key
|
|
76
|
+
? { extsk_hex: params.key.extskHex }
|
|
77
|
+
: { dfvk_hex: params.key.dfvkHex };
|
|
78
|
+
const spec = JSON.stringify({ ...keyField, tree, outputs });
|
|
79
|
+
const detected = await prover(spec);
|
|
80
|
+
return detected
|
|
81
|
+
.filter((n) => !spentNullifiers.has(n.nullifier_hex))
|
|
82
|
+
.map((n) => {
|
|
83
|
+
const txid = txidByKey.get(`${n.height}:${n.tx_index}`);
|
|
84
|
+
if (txid === undefined) {
|
|
85
|
+
throw new ShieldedInputError(`detected note at ${n.height}:${n.tx_index} has no matching block tx (transport/scan mismatch)`);
|
|
86
|
+
}
|
|
87
|
+
// Compact-output values are server-controlled and unauthenticated. Guard the
|
|
88
|
+
// float64 crossing (JSON number) so an out-of-range value throws instead of
|
|
89
|
+
// silently rounding — the money invariant, at this inbound boundary.
|
|
90
|
+
if (!Number.isSafeInteger(n.value)) {
|
|
91
|
+
throw new ShieldedInputError(`detected note value ${n.value} is not a safe integer`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
txid,
|
|
95
|
+
outputIndex: n.output_index,
|
|
96
|
+
height: n.height,
|
|
97
|
+
position: n.position,
|
|
98
|
+
valueSats: BigInt(n.value),
|
|
99
|
+
recipientHex: n.recipient_hex,
|
|
100
|
+
nullifierHex: n.nullifier_hex,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Default sanity ceiling on the fee: 0.1 coin. Overridable via `maxFeeSats`. */
|
|
105
|
+
const DEFAULT_MAX_FEE_SATS = 10000000n;
|
|
106
|
+
/**
|
|
107
|
+
* Build + sign a z→z / z→t spend of a single detected note. Fetches the note's
|
|
108
|
+
* creating output (via `getTransaction`), the tree state at its block − 1, and
|
|
109
|
+
* that block's ordered cmus (to place the note), assembles the prover spec, and
|
|
110
|
+
* runs `prover` (wasm `spendShielded`, typically in a Web Worker). Returns the
|
|
111
|
+
* broadcastable transaction hex.
|
|
112
|
+
*
|
|
113
|
+
* Value conservation is enforced HERE, before signing: `note.valueSats` must
|
|
114
|
+
* equal Σ shielded-output + Σ transparent-output + `feeSats`. The daemon only
|
|
115
|
+
* rejects a *negative* fee at broadcast; a positive overshoot (e.g. a forgotten
|
|
116
|
+
* change output) is a valid, eagerly-mined transaction that donates the excess
|
|
117
|
+
* to miners. Requiring an explicit `feeSats` and checking the balance turns that
|
|
118
|
+
* silent loss into a caught error.
|
|
119
|
+
*/
|
|
120
|
+
export async function buildShieldedSpend(transport, prover, params) {
|
|
121
|
+
const shieldedOutputs = params.shieldedOutputs ?? [];
|
|
122
|
+
const transparentOutputs = params.transparentOutputs ?? [];
|
|
123
|
+
if (shieldedOutputs.length === 0 && transparentOutputs.length === 0) {
|
|
124
|
+
throw new ShieldedInputError('at least one shielded or transparent output is required');
|
|
125
|
+
}
|
|
126
|
+
shieldedOutputs.forEach((o, i) => assertSats(`shieldedOutputs[${i}].valueSats`, o.valueSats));
|
|
127
|
+
transparentOutputs.forEach((o, i) => assertSats(`transparentOutputs[${i}].valueSats`, o.valueSats));
|
|
128
|
+
assertSats('note.valueSats', params.note.valueSats);
|
|
129
|
+
assertSats('feeSats', params.feeSats);
|
|
130
|
+
// Value conservation + a fee sanity ceiling — see the doc comment above.
|
|
131
|
+
const maxFee = params.maxFeeSats ?? DEFAULT_MAX_FEE_SATS;
|
|
132
|
+
if (params.feeSats > maxFee) {
|
|
133
|
+
throw new ShieldedInputError(`feeSats ${params.feeSats} exceeds maxFeeSats ${maxFee} — pass maxFeeSats to override`);
|
|
134
|
+
}
|
|
135
|
+
const outSum = shieldedOutputs.reduce((a, o) => a + o.valueSats, 0n) +
|
|
136
|
+
transparentOutputs.reduce((a, o) => a + o.valueSats, 0n);
|
|
137
|
+
if (params.note.valueSats !== outSum + params.feeSats) {
|
|
138
|
+
throw new ShieldedInputError(`value conservation failed: note ${params.note.valueSats} != outputs ${outSum} + fee ${params.feeSats}`);
|
|
139
|
+
}
|
|
140
|
+
const tx = await transport.getTransaction(params.note.txid);
|
|
141
|
+
const height = Number(tx.height);
|
|
142
|
+
const out = parseSaplingOutput(tx.data, params.note.outputIndex);
|
|
143
|
+
const treeState = await transport.getTreeState(height - 1);
|
|
144
|
+
const tree = parseTreeState(treeState.tree);
|
|
145
|
+
const blockCmus = [];
|
|
146
|
+
for await (const block of transport.getBlockRange(height, height)) {
|
|
147
|
+
for (const btx of block.vtx)
|
|
148
|
+
for (const o of btx.outputs)
|
|
149
|
+
blockCmus.push(toHex(o.cmu));
|
|
150
|
+
}
|
|
151
|
+
const myCmuIndex = blockCmus.indexOf(out.cmu);
|
|
152
|
+
if (myCmuIndex < 0) {
|
|
153
|
+
throw new ShieldedInputError(`note cmu not found in block ${height} compact outputs`);
|
|
154
|
+
}
|
|
155
|
+
const expiryHeight = params.expiryHeight ?? (await transport.getLatestHeight()) + 40;
|
|
156
|
+
const spec = {
|
|
157
|
+
extsk_hex: params.note.extskHex,
|
|
158
|
+
out,
|
|
159
|
+
tree,
|
|
160
|
+
block_cmus: blockCmus,
|
|
161
|
+
my_cmu_index: myCmuIndex,
|
|
162
|
+
shielded_outputs: shieldedOutputs.map((o) => ({
|
|
163
|
+
recipient_hex: saplingAddressToHex(o.address),
|
|
164
|
+
value: toSafeNumber(o.valueSats),
|
|
165
|
+
...(o.memoHex !== undefined ? { memo_hex: o.memoHex } : { memo: o.memo ?? '' }),
|
|
166
|
+
})),
|
|
167
|
+
transparent_outputs: transparentOutputs.map((o) => ({
|
|
168
|
+
value: toSafeNumber(o.valueSats),
|
|
169
|
+
script_hex: o.scriptHex,
|
|
170
|
+
})),
|
|
171
|
+
// The Rust builder re-checks conservation against the DECRYPTED note value
|
|
172
|
+
// (authoritative) once the wasm is rebuilt to read this field.
|
|
173
|
+
fee: toSafeNumber(params.feeSats),
|
|
174
|
+
expiry_height: expiryHeight,
|
|
175
|
+
branch_id: params.branchId ?? CONSENSUS_BRANCH_ID,
|
|
176
|
+
};
|
|
177
|
+
const hex = await prover(JSON.stringify(spec));
|
|
178
|
+
return { hex };
|
|
179
|
+
}
|
package/dist/wasm.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript loader for the Sapling proving wasm module.
|
|
3
|
+
*
|
|
4
|
+
* The wasm (built from `crate/` via `wasm-pack build --target web`, output in
|
|
5
|
+
* `crate/pkg/`) exposes two builders. The caller supplies the two Sapling params
|
|
6
|
+
* files (byte-identical to Zcash's `sapling-spend.params` / `sapling-output.params`,
|
|
7
|
+
* ~50MB total) and a JSON request; the builders return the signed transaction hex.
|
|
8
|
+
*
|
|
9
|
+
* Proving is CPU-heavy (~5s single-threaded per tx) — run it off the main thread
|
|
10
|
+
* (a Web Worker in the browser); see `browser/worker-prover.ts`.
|
|
11
|
+
*
|
|
12
|
+
* STATUS: verified end-to-end — a t->z built through this wasm in Node was
|
|
13
|
+
* accepted by the Verus testnet daemon (txid d142edf8…a0ef).
|
|
14
|
+
*/
|
|
15
|
+
/** SHA-256 of the canonical Zcash Sapling proving parameters (byte-identical for
|
|
16
|
+
* Verus). See NOTICE / README. */
|
|
17
|
+
export declare const PARAM_SHA256: {
|
|
18
|
+
readonly spend: "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13";
|
|
19
|
+
readonly output: "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Verify caller-supplied proving parameters are the canonical Sapling params
|
|
23
|
+
* before they are fed to the prover. The prover reads them WITHOUT group-element
|
|
24
|
+
* checks (for speed), so an unverified/malicious params file is attack surface;
|
|
25
|
+
* this hash gate is the cheap defense (one SHA-256 of ~50 MB). Throws on mismatch.
|
|
26
|
+
* Call once after loading params (the browser Web Worker does this on init).
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifyCanonicalParams(params: SaplingParams): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the wasm module. Pass the `.wasm` bytes/URL/Response; in Node,
|
|
31
|
+
* read `crate/pkg/verus_sapling_prover_bg.wasm` and pass the bytes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function initSapling(wasm: BufferSource | URL | Response | Promise<Response>): Promise<void>;
|
|
34
|
+
/** Params, loaded by the caller (fetch / fs). */
|
|
35
|
+
export interface SaplingParams {
|
|
36
|
+
spend: Uint8Array;
|
|
37
|
+
output: Uint8Array;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build + sign a t->z shielding tx from a JSON spec (see the Rust
|
|
41
|
+
* `json_api::build_t2z_from_json` shape). Returns tx hex; the consumer still has
|
|
42
|
+
* the daemon fill the transparent scriptSig via `signrawtransaction`.
|
|
43
|
+
* Call `initSapling` first.
|
|
44
|
+
*/
|
|
45
|
+
export declare function shieldT2z(specJson: string, params: SaplingParams): string;
|
|
46
|
+
/**
|
|
47
|
+
* Build + sign a z->z / z->t tx that spends a shielded note (JSON spec per
|
|
48
|
+
* `json_api::build_zspend_from_json`). Complete after this (no transparent
|
|
49
|
+
* inputs) — broadcast directly. Call `initSapling` first.
|
|
50
|
+
*/
|
|
51
|
+
export declare function spendShielded(specJson: string, params: SaplingParams): string;
|
|
52
|
+
/** A note detected by trial-decryption (see `detectNotes`). All 32-byte fields hex. */
|
|
53
|
+
export interface DetectedNoteRaw {
|
|
54
|
+
height: number;
|
|
55
|
+
tx_index: number;
|
|
56
|
+
output_index: number;
|
|
57
|
+
/** 0-based leaf position in the note-commitment tree. */
|
|
58
|
+
position: number;
|
|
59
|
+
/** Note value in zatoshi (safe: below 2^53 for VRSC supply). */
|
|
60
|
+
value: number;
|
|
61
|
+
/** 43-byte Sapling payment address the note pays to (one of the wallet's). */
|
|
62
|
+
recipient_hex: string;
|
|
63
|
+
/** Note nullifier — appears in a future spend's vShieldedSpend. */
|
|
64
|
+
nullifier_hex: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Trial-decrypt compact Sapling outputs to find the wallet's own notes (read
|
|
68
|
+
* path — no params, no proving; runs in milliseconds). `specJson` is the
|
|
69
|
+
* note-detection request (see the Rust `json_api::detect_notes_from_json`).
|
|
70
|
+
* Does NOT require `initSapling` params, but the wasm module must be
|
|
71
|
+
* initialized. Safe on the main thread.
|
|
72
|
+
*/
|
|
73
|
+
export declare function detectNotes(specJson: string): DetectedNoteRaw[];
|
|
74
|
+
/** A fully-decrypted incoming note (see `readNote`). */
|
|
75
|
+
export interface ReadNoteResult {
|
|
76
|
+
/** Note value in zatoshi. */
|
|
77
|
+
value: number;
|
|
78
|
+
/** 43-byte Sapling payment address the note pays to (hex). */
|
|
79
|
+
recipient_hex: string;
|
|
80
|
+
/** Full 512-byte memo as hex. */
|
|
81
|
+
memo_hex: string;
|
|
82
|
+
/** Memo decoded as UTF-8 text (trimmed at the first 0x00/0xf6 pad byte). */
|
|
83
|
+
memo_text: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fully decrypt one incoming output — value + recipient + memo — with the
|
|
87
|
+
* wallet's viewing key. `specJson` is the read request (see the Rust
|
|
88
|
+
* `json_api::read_note_from_json`): `{ extsk_hex | dfvk_hex, out:{cv,cmu,epk,enc,ct,proof} }`.
|
|
89
|
+
* Returns `null` if the output is not for this key. Cheap (no params, no proving).
|
|
90
|
+
*/
|
|
91
|
+
export declare function readNote(specJson: string): ReadNoteResult | null;
|
package/dist/wasm.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript loader for the Sapling proving wasm module.
|
|
3
|
+
*
|
|
4
|
+
* The wasm (built from `crate/` via `wasm-pack build --target web`, output in
|
|
5
|
+
* `crate/pkg/`) exposes two builders. The caller supplies the two Sapling params
|
|
6
|
+
* files (byte-identical to Zcash's `sapling-spend.params` / `sapling-output.params`,
|
|
7
|
+
* ~50MB total) and a JSON request; the builders return the signed transaction hex.
|
|
8
|
+
*
|
|
9
|
+
* Proving is CPU-heavy (~5s single-threaded per tx) — run it off the main thread
|
|
10
|
+
* (a Web Worker in the browser); see `browser/worker-prover.ts`.
|
|
11
|
+
*
|
|
12
|
+
* STATUS: verified end-to-end — a t->z built through this wasm in Node was
|
|
13
|
+
* accepted by the Verus testnet daemon (txid d142edf8…a0ef).
|
|
14
|
+
*/
|
|
15
|
+
// The generated glue is ESM (wasm-pack, crate/pkg). In a bundler/browser, import
|
|
16
|
+
// the pkg directly.
|
|
17
|
+
import initWasm, { shield_t2z, spend_shielded, detect_notes, read_note, } from '../crate/pkg/verus_sapling_prover.js';
|
|
18
|
+
import { ShieldedError } from './errors.js';
|
|
19
|
+
let ready;
|
|
20
|
+
/** SHA-256 of the canonical Zcash Sapling proving parameters (byte-identical for
|
|
21
|
+
* Verus). See NOTICE / README. */
|
|
22
|
+
export const PARAM_SHA256 = {
|
|
23
|
+
spend: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13',
|
|
24
|
+
output: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4',
|
|
25
|
+
};
|
|
26
|
+
async function sha256Hex(bytes) {
|
|
27
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
28
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Verify caller-supplied proving parameters are the canonical Sapling params
|
|
32
|
+
* before they are fed to the prover. The prover reads them WITHOUT group-element
|
|
33
|
+
* checks (for speed), so an unverified/malicious params file is attack surface;
|
|
34
|
+
* this hash gate is the cheap defense (one SHA-256 of ~50 MB). Throws on mismatch.
|
|
35
|
+
* Call once after loading params (the browser Web Worker does this on init).
|
|
36
|
+
*/
|
|
37
|
+
export async function verifyCanonicalParams(params) {
|
|
38
|
+
const [spend, output] = await Promise.all([sha256Hex(params.spend), sha256Hex(params.output)]);
|
|
39
|
+
if (spend !== PARAM_SHA256.spend) {
|
|
40
|
+
throw new ShieldedError('ERR_SHIELDED_PARAMS', `sapling-spend.params SHA-256 mismatch (got ${spend})`);
|
|
41
|
+
}
|
|
42
|
+
if (output !== PARAM_SHA256.output) {
|
|
43
|
+
throw new ShieldedError('ERR_SHIELDED_PARAMS', `sapling-output.params SHA-256 mismatch (got ${output})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the wasm module. Pass the `.wasm` bytes/URL/Response; in Node,
|
|
48
|
+
* read `crate/pkg/verus_sapling_prover_bg.wasm` and pass the bytes.
|
|
49
|
+
*/
|
|
50
|
+
export function initSapling(wasm) {
|
|
51
|
+
if (!ready)
|
|
52
|
+
ready = initWasm({ module_or_path: wasm }).then(() => undefined);
|
|
53
|
+
return ready;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build + sign a t->z shielding tx from a JSON spec (see the Rust
|
|
57
|
+
* `json_api::build_t2z_from_json` shape). Returns tx hex; the consumer still has
|
|
58
|
+
* the daemon fill the transparent scriptSig via `signrawtransaction`.
|
|
59
|
+
* Call `initSapling` first.
|
|
60
|
+
*/
|
|
61
|
+
export function shieldT2z(specJson, params) {
|
|
62
|
+
return shield_t2z(specJson, params.spend, params.output);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build + sign a z->z / z->t tx that spends a shielded note (JSON spec per
|
|
66
|
+
* `json_api::build_zspend_from_json`). Complete after this (no transparent
|
|
67
|
+
* inputs) — broadcast directly. Call `initSapling` first.
|
|
68
|
+
*/
|
|
69
|
+
export function spendShielded(specJson, params) {
|
|
70
|
+
return spend_shielded(specJson, params.spend, params.output);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Trial-decrypt compact Sapling outputs to find the wallet's own notes (read
|
|
74
|
+
* path — no params, no proving; runs in milliseconds). `specJson` is the
|
|
75
|
+
* note-detection request (see the Rust `json_api::detect_notes_from_json`).
|
|
76
|
+
* Does NOT require `initSapling` params, but the wasm module must be
|
|
77
|
+
* initialized. Safe on the main thread.
|
|
78
|
+
*/
|
|
79
|
+
export function detectNotes(specJson) {
|
|
80
|
+
return JSON.parse(detect_notes(specJson));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Fully decrypt one incoming output — value + recipient + memo — with the
|
|
84
|
+
* wallet's viewing key. `specJson` is the read request (see the Rust
|
|
85
|
+
* `json_api::read_note_from_json`): `{ extsk_hex | dfvk_hex, out:{cv,cmu,epk,enc,ct,proof} }`.
|
|
86
|
+
* Returns `null` if the output is not for this key. Cheap (no params, no proving).
|
|
87
|
+
*/
|
|
88
|
+
export function readNote(specJson) {
|
|
89
|
+
return JSON.parse(read_note(specJson));
|
|
90
|
+
}
|
package/dist/zaddr.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode a Verus Sapling bech32 address to its raw 43-byte payment-address
|
|
3
|
+
* payload — the form the Rust/wasm prover expects.
|
|
4
|
+
*
|
|
5
|
+
* HRP note (verified against a live daemon, not the stock-zcash source): real
|
|
6
|
+
* Verus uses the `zs` prefix on BOTH mainnet and vrsctest — there is no
|
|
7
|
+
* HRP-level network distinction, so the payload alone determines the recipient.
|
|
8
|
+
* We also tolerate stock-zcash `ztestsapling` for compatibility, but Verus
|
|
9
|
+
* itself never emits it. The bech32 checksum is the only guard against a
|
|
10
|
+
* mistyped address; `bech32Decode` verifies it.
|
|
11
|
+
*
|
|
12
|
+
* Minimal bech32 (BIP-173) decoder, no dependency (keeps the tiny-dep ethos).
|
|
13
|
+
* Sapling addresses use bech32 (not bech32m) and carry an 88-symbol data part
|
|
14
|
+
* that converts to 43 bytes (11-byte diversifier + 32-byte pk_d).
|
|
15
|
+
*/
|
|
16
|
+
/** Decode a `zs`/`ztestsapling` address to its 43-byte payload. */
|
|
17
|
+
export declare function decodeSaplingAddress(addr: string): Uint8Array;
|
|
18
|
+
/** Hex of the 43-byte payload (convenience for JSON specs). */
|
|
19
|
+
export declare function saplingAddressToHex(addr: string): string;
|
package/dist/zaddr.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode a Verus Sapling bech32 address to its raw 43-byte payment-address
|
|
3
|
+
* payload — the form the Rust/wasm prover expects.
|
|
4
|
+
*
|
|
5
|
+
* HRP note (verified against a live daemon, not the stock-zcash source): real
|
|
6
|
+
* Verus uses the `zs` prefix on BOTH mainnet and vrsctest — there is no
|
|
7
|
+
* HRP-level network distinction, so the payload alone determines the recipient.
|
|
8
|
+
* We also tolerate stock-zcash `ztestsapling` for compatibility, but Verus
|
|
9
|
+
* itself never emits it. The bech32 checksum is the only guard against a
|
|
10
|
+
* mistyped address; `bech32Decode` verifies it.
|
|
11
|
+
*
|
|
12
|
+
* Minimal bech32 (BIP-173) decoder, no dependency (keeps the tiny-dep ethos).
|
|
13
|
+
* Sapling addresses use bech32 (not bech32m) and carry an 88-symbol data part
|
|
14
|
+
* that converts to 43 bytes (11-byte diversifier + 32-byte pk_d).
|
|
15
|
+
*/
|
|
16
|
+
import { bytesToHex } from './hex.js';
|
|
17
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
18
|
+
const BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
19
|
+
/** BIP-173 polymod over 5-bit values (bit ops stay within 30 bits, safe in JS). */
|
|
20
|
+
function bech32Polymod(values) {
|
|
21
|
+
let chk = 1;
|
|
22
|
+
for (const v of values) {
|
|
23
|
+
const top = chk >>> 25;
|
|
24
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
25
|
+
for (let i = 0; i < 5; i++)
|
|
26
|
+
if ((top >> i) & 1)
|
|
27
|
+
chk ^= BECH32_GENERATOR[i];
|
|
28
|
+
}
|
|
29
|
+
return chk >>> 0;
|
|
30
|
+
}
|
|
31
|
+
function hrpExpand(hrp) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < hrp.length; i++)
|
|
34
|
+
out.push(hrp.charCodeAt(i) >> 5);
|
|
35
|
+
out.push(0);
|
|
36
|
+
for (let i = 0; i < hrp.length; i++)
|
|
37
|
+
out.push(hrp.charCodeAt(i) & 31);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function bech32Decode(addr) {
|
|
41
|
+
const raw = addr.trim();
|
|
42
|
+
// BIP-173: a decoder MUST reject a string that mixes upper- and lower-case.
|
|
43
|
+
if (/[a-z]/.test(raw) && /[A-Z]/.test(raw))
|
|
44
|
+
throw new Error('bech32: mixed case');
|
|
45
|
+
const s = raw.toLowerCase();
|
|
46
|
+
const sep = s.lastIndexOf('1');
|
|
47
|
+
if (sep < 1)
|
|
48
|
+
throw new Error('invalid bech32: no separator');
|
|
49
|
+
const hrp = s.slice(0, sep);
|
|
50
|
+
const data = [];
|
|
51
|
+
for (const ch of s.slice(sep + 1)) {
|
|
52
|
+
const v = CHARSET.indexOf(ch);
|
|
53
|
+
if (v === -1)
|
|
54
|
+
throw new Error(`invalid bech32 char: ${ch}`);
|
|
55
|
+
data.push(v);
|
|
56
|
+
}
|
|
57
|
+
if (data.length < 6)
|
|
58
|
+
throw new Error('bech32 data too short');
|
|
59
|
+
// Verify the 6-symbol checksum (Sapling uses bech32, constant 1 — not bech32m)
|
|
60
|
+
// BEFORE stripping it. Skipping this would let a single mistyped character
|
|
61
|
+
// decode to a valid-looking but WRONG 43-byte payload — on mainnet, funds
|
|
62
|
+
// encrypted to an unspendable key.
|
|
63
|
+
if (bech32Polymod([...hrpExpand(hrp), ...data]) !== 1) {
|
|
64
|
+
throw new Error('invalid bech32 checksum');
|
|
65
|
+
}
|
|
66
|
+
return { hrp, data: data.slice(0, -6) };
|
|
67
|
+
}
|
|
68
|
+
function convertBits(data, from, to, pad) {
|
|
69
|
+
let acc = 0;
|
|
70
|
+
let bits = 0;
|
|
71
|
+
const out = [];
|
|
72
|
+
const maxv = (1 << to) - 1;
|
|
73
|
+
for (const value of data) {
|
|
74
|
+
acc = (acc << from) | value;
|
|
75
|
+
bits += from;
|
|
76
|
+
while (bits >= to) {
|
|
77
|
+
bits -= to;
|
|
78
|
+
out.push((acc >> bits) & maxv);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (pad) {
|
|
82
|
+
if (bits > 0)
|
|
83
|
+
out.push((acc << (to - bits)) & maxv);
|
|
84
|
+
}
|
|
85
|
+
else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
|
|
86
|
+
// BIP-173: reject non-canonical trailing bits (too many leftover, or padding
|
|
87
|
+
// bits not zero) so a valid-checksum address can't have a malleable encoding.
|
|
88
|
+
throw new Error('bech32: non-canonical padding');
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** Decode a `zs`/`ztestsapling` address to its 43-byte payload. */
|
|
93
|
+
export function decodeSaplingAddress(addr) {
|
|
94
|
+
const { hrp, data } = bech32Decode(addr);
|
|
95
|
+
if (hrp !== 'zs' && hrp !== 'ztestsapling') {
|
|
96
|
+
throw new Error(`not a Sapling address (hrp=${hrp})`);
|
|
97
|
+
}
|
|
98
|
+
const bytes = convertBits(data, 5, 8, false);
|
|
99
|
+
if (bytes.length !== 43) {
|
|
100
|
+
throw new Error(`expected 43-byte payload, got ${bytes.length}`);
|
|
101
|
+
}
|
|
102
|
+
return Uint8Array.from(bytes);
|
|
103
|
+
}
|
|
104
|
+
/** Hex of the 43-byte payload (convenience for JSON specs). */
|
|
105
|
+
export function saplingAddressToHex(addr) {
|
|
106
|
+
return bytesToHex(decodeSaplingAddress(addr));
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chainvue/verus-sapling",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Offline signing of Verus shielded (Sapling) transactions — t→z, z→z, z→t, with memo. Companion to @chainvue/verus-sdk. Builds and signs bytes; consumers broadcast. WASM prover, no full node on the signing host.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Robert Lech",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/chainvue/verus-sapling.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/chainvue/verus-sapling#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/chainvue/verus-sapling/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"verus",
|
|
17
|
+
"zcash",
|
|
18
|
+
"sapling",
|
|
19
|
+
"shielded",
|
|
20
|
+
"zk-snark",
|
|
21
|
+
"offline-signing",
|
|
22
|
+
"wasm",
|
|
23
|
+
"typescript"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"crate/pkg",
|
|
31
|
+
"proto",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"NOTICE",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./lightwalletd": {
|
|
42
|
+
"types": "./dist/lightwalletd.d.ts",
|
|
43
|
+
"default": "./dist/lightwalletd.js"
|
|
44
|
+
},
|
|
45
|
+
"./browser": {
|
|
46
|
+
"types": "./dist/browser/index.d.ts",
|
|
47
|
+
"default": "./dist/browser/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./prover-worker": {
|
|
50
|
+
"types": "./dist/browser/prover-worker.d.ts",
|
|
51
|
+
"default": "./dist/browser/prover-worker.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc",
|
|
59
|
+
"build:ext": "tsc && node examples/extension/build.mjs",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"lint": "eslint .",
|
|
63
|
+
"prepack": "npm run build"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@chainvue/verus-sdk": ">=0.14.0"
|
|
67
|
+
},
|
|
68
|
+
"optionalDependencies": {
|
|
69
|
+
"@grpc/grpc-js": "^1.10.0",
|
|
70
|
+
"@grpc/proto-loader": "^0.7.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@chainvue/verus-sdk": ">=0.14.0",
|
|
74
|
+
"@grpc/grpc-js": "^1.10.0",
|
|
75
|
+
"@grpc/proto-loader": "^0.7.0",
|
|
76
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
77
|
+
"@semantic-release/exec": "^7.1.0",
|
|
78
|
+
"@semantic-release/git": "^10.0.1",
|
|
79
|
+
"@types/node": "^22",
|
|
80
|
+
"esbuild": "^0.24.2",
|
|
81
|
+
"semantic-release": "^25.0.7",
|
|
82
|
+
"typescript": "^5",
|
|
83
|
+
"vitest": "^2"
|
|
84
|
+
},
|
|
85
|
+
"//": "The concrete lightwalletd gRPC client (@chainvue/verus-sapling/lightwalletd) needs @grpc/grpc-js — an OPTIONAL dependency: the browser path implements LightwalletdTransport over gRPC-web instead, and the package root pulls in no gRPC at runtime.",
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public",
|
|
88
|
+
"provenance": true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) 2019-2020 The Zcash developers
|
|
2
|
+
// Distributed under the MIT software license, see the accompanying
|
|
3
|
+
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
|
4
|
+
|
|
5
|
+
syntax = "proto3";
|
|
6
|
+
package cash.z.wallet.sdk.rpc;
|
|
7
|
+
option go_package = "lightwalletd/walletrpc";
|
|
8
|
+
option swift_prefix = "";
|
|
9
|
+
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
|
10
|
+
// bytes fields of hashes are in canonical little-endian format.
|
|
11
|
+
|
|
12
|
+
// Information about the state of the chain as of a given block.
|
|
13
|
+
message ChainMetadata {
|
|
14
|
+
optional uint64 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
|
18
|
+
// 1. Detect a payment to your shielded Sapling address
|
|
19
|
+
// 2. Detect a spend of your shielded Sapling notes
|
|
20
|
+
// 3. Update your witnesses to generate new Sapling spend proofs.
|
|
21
|
+
message CompactBlock {
|
|
22
|
+
uint32 protoVersion = 1;
|
|
23
|
+
uint64 height = 2;
|
|
24
|
+
bytes hash = 3;
|
|
25
|
+
bytes prevHash = 4;
|
|
26
|
+
uint32 time = 5;
|
|
27
|
+
bytes header = 6;
|
|
28
|
+
repeated CompactTx vtx = 7;
|
|
29
|
+
ChainMetadata chainMetadata = 8;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// CompactTx contains the minimum information for a wallet to know if this transaction
|
|
33
|
+
// is relevant to it (either pays to it or spends from it) via shielded elements
|
|
34
|
+
// only. This message will not encode a transparent-to-transparent transaction.
|
|
35
|
+
message CompactTx {
|
|
36
|
+
uint64 index = 1; // the index within the full block
|
|
37
|
+
bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers
|
|
38
|
+
|
|
39
|
+
// The transaction fee: present if server can provide. In the case of a
|
|
40
|
+
// stateless server and a transaction with transparent inputs, this will be
|
|
41
|
+
// unset because the calculation requires reference to prior transactions.
|
|
42
|
+
// in a pure-Sapling context, the fee will be calculable as:
|
|
43
|
+
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
|
44
|
+
uint32 fee = 3;
|
|
45
|
+
|
|
46
|
+
repeated CompactSpend spends = 4; // inputs
|
|
47
|
+
repeated CompactOutput outputs = 5; // outputs
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// CompactSpend is a Sapling Spend Description as described in 7.3 of the Zcash
|
|
51
|
+
// protocol specification.
|
|
52
|
+
message CompactSpend {
|
|
53
|
+
bytes nf = 1; // nullifier (see the Zcash protocol specification)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// output is a Sapling Output Description as described in section 7.4 of the
|
|
57
|
+
// Zcash protocol spec. Total size is 948.
|
|
58
|
+
message CompactOutput {
|
|
59
|
+
bytes cmu = 1; // note commitment u-coordinate
|
|
60
|
+
bytes epk = 2; // ephemeral public key
|
|
61
|
+
bytes ciphertext = 3; // ciphertext and zkproof
|
|
62
|
+
}
|