@zbase-protocol/core 0.1.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 +201 -0
- package/NOTICE +20 -0
- package/README.md +118 -0
- package/dist/account.d.ts +79 -0
- package/dist/account.js +140 -0
- package/dist/experimental.d.ts +27 -0
- package/dist/experimental.js +23 -0
- package/dist/facilitatorClient.d.ts +171 -0
- package/dist/facilitatorClient.js +146 -0
- package/dist/forwardingNotes.d.ts +103 -0
- package/dist/forwardingNotes.js +149 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/merkle.d.ts +48 -0
- package/dist/merkle.js +84 -0
- package/dist/noteScanner.d.ts +206 -0
- package/dist/noteScanner.js +422 -0
- package/dist/notes.d.ts +305 -0
- package/dist/notes.js +547 -0
- package/dist/npk.d.ts +121 -0
- package/dist/npk.js +166 -0
- package/dist/proofs.d.ts +66 -0
- package/dist/proofs.js +45 -0
- package/dist/stealth.d.ts +136 -0
- package/dist/stealth.js +387 -0
- package/dist/swapPlan.d.ts +52 -0
- package/dist/swapPlan.js +104 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +4 -0
- package/dist/viewingKeyHD.d.ts +108 -0
- package/dist/viewingKeyHD.js +176 -0
- package/package.json +71 -0
package/dist/npk.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zbase-protocol/core — Recipient NPK derivation (audit C3).
|
|
3
|
+
*
|
|
4
|
+
* The v1 UTXO commitment is `Poseidon3(amount, NPK, secret)` where
|
|
5
|
+
* `NPK = Poseidon2(spendingPK, viewingPKBlind)` (matches `note_spend.circom`
|
|
6
|
+
* NpkBuilder + CommitmentHasher — see `notes.ts`). Phase 1A pinned the *hash
|
|
7
|
+
* structure*; this module closes the remaining "auditor-scoped gap" noted in
|
|
8
|
+
* `notes.ts`: HOW `spendingPK` / `viewingPKBlind` are DERIVED from a recipient's
|
|
9
|
+
* published key material.
|
|
10
|
+
*
|
|
11
|
+
* ## Recipe (Railgun-style note-public-key)
|
|
12
|
+
*
|
|
13
|
+
* spendingPK = field(keccak("zBase/npk/spendingPK/v1" || recipientSpendPub))
|
|
14
|
+
* ephemeral = fresh X25519 keypair (per note)
|
|
15
|
+
* shared = X25519(ephemeralPriv, recipientViewingPub)
|
|
16
|
+
* viewingPKBlind = field(keccak("zBase/npk/viewingPKBlind/v1" || shared))
|
|
17
|
+
* NPK = Poseidon2(spendingPK, viewingPKBlind)
|
|
18
|
+
*
|
|
19
|
+
* Two properties this buys:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Unlinkability.** `viewingPKBlind` is a fresh ECDH blind per note, so two
|
|
22
|
+
* notes to the same recipient produce different NPKs → different commitments.
|
|
23
|
+
* An on-chain observer cannot cluster a recipient's incoming notes.
|
|
24
|
+
* 2. **Recoverability.** The recipient publishes `ephemeralPublicKey` alongside
|
|
25
|
+
* the note (it already rides in the `encryptNote` blob — see `notes.ts`
|
|
26
|
+
* EncryptedNote layout, bytes [0..32)). With their viewing PRIVATE key they
|
|
27
|
+
* recompute the same `shared` → the same `viewingPKBlind` → the same NPK →
|
|
28
|
+
* the same commitment, confirming the note is theirs and that they can later
|
|
29
|
+
* prove `NPK = Poseidon2(spendingPK, viewingPKBlind)` in-circuit.
|
|
30
|
+
*
|
|
31
|
+
* `spendingPK` is long-lived (it identifies the recipient's spend authority and
|
|
32
|
+
* is constant across their notes); only `viewingPKBlind` is per-note. This is the
|
|
33
|
+
* standard Railgun split: the spending key authorizes movement, the blinded
|
|
34
|
+
* viewing key provides per-note privacy.
|
|
35
|
+
*
|
|
36
|
+
* ## What this is NOT
|
|
37
|
+
*
|
|
38
|
+
* The recipient's `spendingPublicKey` is a field element the recipient publishes
|
|
39
|
+
* (e.g., a Poseidon/Baby-Jubjub spend pubkey in a full wallet). zBase does not
|
|
40
|
+
* yet ship a spending-key HD scheme (only the X25519 viewing key — see
|
|
41
|
+
* `viewingKeyHD.ts`), so callers supply `recipientSpendingPubKey` explicitly.
|
|
42
|
+
* Until a spending-key tree lands, a recipient MAY reuse a stable field element
|
|
43
|
+
* (e.g., a hash of their viewing pubkey) as their spendingPK — at the cost of the
|
|
44
|
+
* spend-authority separation, NOT of the per-note unlinkability (which comes from
|
|
45
|
+
* the viewing blind). The derivation here is agnostic to that choice.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ AUDITOR NOTE: this is the SDK-side NPK derivation the circuit comment in
|
|
48
|
+
* `note_spend.circom` (NpkBuilder) and `notes.ts` defer to the external audit.
|
|
49
|
+
* The *circuit* only sees the resulting `NPK` field element; this module decides
|
|
50
|
+
* how it is built off-chain. It must be reviewed before the UTXO pool goes live.
|
|
51
|
+
*/
|
|
52
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
53
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
54
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
55
|
+
import { SNARK_SCALAR_FIELD } from "./account.js";
|
|
56
|
+
// Domain-separation tags — keep each derivation distinct so no two keccak uses of
|
|
57
|
+
// the same input can collide (mirrors notes.ts's view-tag domain separation).
|
|
58
|
+
const DST_SPENDING_PK = "zBase/npk/spendingPK/v1";
|
|
59
|
+
const DST_VIEWING_BLIND = "zBase/npk/viewingPKBlind/v1";
|
|
60
|
+
/**
|
|
61
|
+
* Reduce arbitrary bytes to a BN254 scalar-field element via a domain-separated
|
|
62
|
+
* keccak, using DETERMINISTIC rejection sampling for a uniform result.
|
|
63
|
+
*
|
|
64
|
+
* audit-sweep-2026-06-17 (was a latent MED): the previous version did a naive
|
|
65
|
+
* `keccak(...) % SNARK_SCALAR_FIELD` and a comment claimed the bias was
|
|
66
|
+
* "< 2^-253". That was wrong by ~250 bits: r ≈ 2^253.6, so 2^256 / r = 5 with a
|
|
67
|
+
* large remainder — the lowest ~29% of the field is over-represented, giving a
|
|
68
|
+
* statistical distance of ~2^-4.2 (~5.4%), not 2^-253. For `spendingPK` (public
|
|
69
|
+
* input) the bias is cosmetic, but `viewingPKBlind` is derived from a SECRET
|
|
70
|
+
* (the X25519 shared secret), so a biased reduction on a secret-derived witness
|
|
71
|
+
* is a real (if bounded) uniformity weakness. We now reject any digest in the
|
|
72
|
+
* "extra" top band [LIMIT, 2^256) and re-hash with an incrementing counter byte,
|
|
73
|
+
* so every accepted value is uniform over [0, r). The counter keeps it fully
|
|
74
|
+
* deterministic — the recipient recomputes the identical element. Expected
|
|
75
|
+
* iterations ≈ 1.25; the loop is bounded defensively.
|
|
76
|
+
*/
|
|
77
|
+
export function bytesToField(domain, bytes) {
|
|
78
|
+
const tag = new TextEncoder().encode(domain);
|
|
79
|
+
// Largest multiple of r that fits in 256 bits; digests >= LIMIT are rejected
|
|
80
|
+
// so the accepted range maps uniformly onto [0, r).
|
|
81
|
+
const TWO_256 = 1n << 256n;
|
|
82
|
+
const LIMIT = TWO_256 - (TWO_256 % SNARK_SCALAR_FIELD);
|
|
83
|
+
// 256 counter values is astronomically more than ever needed (P(reject) < 1/5
|
|
84
|
+
// per try); throw rather than bias if somehow exhausted.
|
|
85
|
+
for (let counter = 0; counter < 256; counter++) {
|
|
86
|
+
const input = new Uint8Array(tag.length + bytes.length + 1);
|
|
87
|
+
input.set(tag, 0);
|
|
88
|
+
input.set(bytes, tag.length);
|
|
89
|
+
input[tag.length + bytes.length] = counter; // domain ⊕ bytes ⊕ counter
|
|
90
|
+
const h = keccak_256(input);
|
|
91
|
+
let v = 0n;
|
|
92
|
+
for (const byte of h)
|
|
93
|
+
v = (v << 8n) | BigInt(byte);
|
|
94
|
+
if (v < LIMIT)
|
|
95
|
+
return v % SNARK_SCALAR_FIELD;
|
|
96
|
+
}
|
|
97
|
+
throw new Error("bytesToField: rejection sampling exhausted (unreachable)");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Derive a recipient's NPK inputs for a fresh outbound note.
|
|
101
|
+
*
|
|
102
|
+
* Deterministic given (recipientViewingPubKey, recipientSpendingPubKey,
|
|
103
|
+
* ephemeralPrivateKey): pass a fixed ephemeral key to get reproducible output
|
|
104
|
+
* (used by tests + by the witness fixture builder). With no ephemeral key it is
|
|
105
|
+
* non-deterministic (fresh per-note blind), which is the production default.
|
|
106
|
+
*/
|
|
107
|
+
export function deriveRecipientNPK(params) {
|
|
108
|
+
const { recipientViewingPubKey, recipientSpendingPubKey } = params;
|
|
109
|
+
if (recipientViewingPubKey.length !== 32) {
|
|
110
|
+
throw new Error("deriveRecipientNPK: recipientViewingPubKey must be 32 bytes (X25519)");
|
|
111
|
+
}
|
|
112
|
+
if (recipientSpendingPubKey < 0n ||
|
|
113
|
+
recipientSpendingPubKey >= SNARK_SCALAR_FIELD) {
|
|
114
|
+
throw new Error("deriveRecipientNPK: recipientSpendingPubKey must be a field element in [0, r)");
|
|
115
|
+
}
|
|
116
|
+
const ephPriv = params.ephemeralPrivateKey ?? randomBytes(32);
|
|
117
|
+
if (ephPriv.length !== 32) {
|
|
118
|
+
throw new Error("deriveRecipientNPK: ephemeralPrivateKey must be 32 bytes");
|
|
119
|
+
}
|
|
120
|
+
const ephemeralPublicKey = x25519.getPublicKey(ephPriv);
|
|
121
|
+
// spendingPK: long-lived, domain-separated reduction of the recipient's spend
|
|
122
|
+
// pubkey into the field. Constant across the recipient's notes.
|
|
123
|
+
const spendingPK = bytesToField(DST_SPENDING_PK, feToBE32(recipientSpendingPubKey));
|
|
124
|
+
// viewingPKBlind: per-note ECDH blind. shared = X25519(ephPriv, recipientView).
|
|
125
|
+
const shared = x25519.getSharedSecret(ephPriv, recipientViewingPubKey);
|
|
126
|
+
const viewingPKBlind = bytesToField(DST_VIEWING_BLIND, shared);
|
|
127
|
+
return { spendingPK, viewingPKBlind, ephemeralPublicKey };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Recipient-side recovery of `viewingPKBlind` from the published ephemeral
|
|
131
|
+
* pubkey + the recipient's viewing PRIVATE key. Recomputes the same ECDH shared
|
|
132
|
+
* secret the sender used, so the recipient can reconstruct the NPK (and hence the
|
|
133
|
+
* commitment) and confirm the note is addressed to them.
|
|
134
|
+
*
|
|
135
|
+
* shared = X25519(viewingPriv, ephemeralPub) // == sender's X25519(ephPriv, viewingPub)
|
|
136
|
+
* viewingPKBlind = field(keccak(DST || shared))
|
|
137
|
+
*/
|
|
138
|
+
export function recoverViewingPKBlind(viewingPrivateKey, ephemeralPublicKey) {
|
|
139
|
+
if (viewingPrivateKey.length !== 32) {
|
|
140
|
+
throw new Error("recoverViewingPKBlind: viewingPrivateKey must be 32 bytes");
|
|
141
|
+
}
|
|
142
|
+
if (ephemeralPublicKey.length !== 32) {
|
|
143
|
+
throw new Error("recoverViewingPKBlind: ephemeralPublicKey must be 32 bytes");
|
|
144
|
+
}
|
|
145
|
+
const shared = x25519.getSharedSecret(viewingPrivateKey, ephemeralPublicKey);
|
|
146
|
+
return bytesToField(DST_VIEWING_BLIND, shared);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Big-endian 32-byte encoding of a field element. Local copy of `account.ts`'s
|
|
150
|
+
* `feToBE32` value semantics (kept here to avoid a string round-trip — account's
|
|
151
|
+
* takes string|bigint and returns via a different path) so the spendingPK
|
|
152
|
+
* reduction has a fixed-width, canonical input.
|
|
153
|
+
*/
|
|
154
|
+
function feToBE32(value) {
|
|
155
|
+
if (value < 0n)
|
|
156
|
+
throw new Error("feToBE32: value must be non-negative");
|
|
157
|
+
const out = new Uint8Array(32);
|
|
158
|
+
let v = value;
|
|
159
|
+
for (let i = 31; i >= 0; i--) {
|
|
160
|
+
out[i] = Number(v & 0xffn);
|
|
161
|
+
v >>= 8n;
|
|
162
|
+
}
|
|
163
|
+
if (v !== 0n)
|
|
164
|
+
throw new Error("feToBE32: value exceeds 32 bytes");
|
|
165
|
+
return out;
|
|
166
|
+
}
|
package/dist/proofs.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zbase-protocol/core — Chain-agnostic ZK proof generation
|
|
3
|
+
*
|
|
4
|
+
* Uses snarkjs for Groth16 proof generation. The circuits are universal:
|
|
5
|
+
* same WASM + zkey files work regardless of which chain the proof is verified on.
|
|
6
|
+
*
|
|
7
|
+
* snarkjs is an OPTIONAL dependency (SDK audit 2026-07-09): it is ~9 MB and only
|
|
8
|
+
* needed by consumers who actually GENERATE a proof. It is loaded LAZILY inside
|
|
9
|
+
* the two functions below, so a consumer using only the Poseidon/Merkle/stealth/
|
|
10
|
+
* facilitator primitives never pulls it in. If snarkjs isn't installed, the lazy
|
|
11
|
+
* import throws a clear message pointing the consumer to add it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Inputs to the upstream withdraw.circom (template `Withdraw(maxTreeDepth=32)`).
|
|
15
|
+
* Order and names mirror the circuit's signal declarations exactly. Padding:
|
|
16
|
+
* `stateSiblings` and `ASPSiblings` MUST be padded with zeros to length 32.
|
|
17
|
+
*
|
|
18
|
+
* Public signals returned by snarkjs (in order):
|
|
19
|
+
* [0] newCommitmentHash (output)
|
|
20
|
+
* [1] existingNullifierHash (output)
|
|
21
|
+
* [2] withdrawnValue
|
|
22
|
+
* [3] stateRoot
|
|
23
|
+
* [4] stateTreeDepth
|
|
24
|
+
* [5] ASPRoot
|
|
25
|
+
* [6] ASPTreeDepth
|
|
26
|
+
* [7] context
|
|
27
|
+
*/
|
|
28
|
+
export interface ProofInput {
|
|
29
|
+
withdrawnValue: string;
|
|
30
|
+
stateRoot: string;
|
|
31
|
+
stateTreeDepth: string;
|
|
32
|
+
ASPRoot: string;
|
|
33
|
+
ASPTreeDepth: string;
|
|
34
|
+
context: string;
|
|
35
|
+
label: string;
|
|
36
|
+
existingValue: string;
|
|
37
|
+
existingNullifier: string;
|
|
38
|
+
existingSecret: string;
|
|
39
|
+
newNullifier: string;
|
|
40
|
+
newSecret: string;
|
|
41
|
+
stateSiblings: string[];
|
|
42
|
+
stateIndex: string;
|
|
43
|
+
ASPSiblings: string[];
|
|
44
|
+
ASPIndex: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Groth16Proof {
|
|
47
|
+
pi_a: [string, string];
|
|
48
|
+
pi_b: [[string, string], [string, string]];
|
|
49
|
+
pi_c: [string, string];
|
|
50
|
+
protocol: "groth16";
|
|
51
|
+
curve: "bn128";
|
|
52
|
+
}
|
|
53
|
+
export interface ProofResult {
|
|
54
|
+
proof: Groth16Proof;
|
|
55
|
+
publicSignals: string[];
|
|
56
|
+
proofTimeMs: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate a Groth16 withdrawal proof.
|
|
60
|
+
* Chain-agnostic: works for both EVM and Solana verifiers.
|
|
61
|
+
*/
|
|
62
|
+
export declare function generateWithdrawalProof(input: ProofInput, wasmPath: string, zkeyPath: string): Promise<ProofResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Verify a Groth16 proof locally (for testing, not on-chain verification).
|
|
65
|
+
*/
|
|
66
|
+
export declare function verifyProofLocally(proof: Groth16Proof, publicSignals: string[], vkeyPath: string): Promise<boolean>;
|
package/dist/proofs.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zbase-protocol/core — Chain-agnostic ZK proof generation
|
|
3
|
+
*
|
|
4
|
+
* Uses snarkjs for Groth16 proof generation. The circuits are universal:
|
|
5
|
+
* same WASM + zkey files work regardless of which chain the proof is verified on.
|
|
6
|
+
*
|
|
7
|
+
* snarkjs is an OPTIONAL dependency (SDK audit 2026-07-09): it is ~9 MB and only
|
|
8
|
+
* needed by consumers who actually GENERATE a proof. It is loaded LAZILY inside
|
|
9
|
+
* the two functions below, so a consumer using only the Poseidon/Merkle/stealth/
|
|
10
|
+
* facilitator primitives never pulls it in. If snarkjs isn't installed, the lazy
|
|
11
|
+
* import throws a clear message pointing the consumer to add it.
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
async function loadSnarkjs() {
|
|
15
|
+
try {
|
|
16
|
+
const requireCjs = createRequire(import.meta.url);
|
|
17
|
+
return requireCjs("snarkjs");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error("snarkjs is required for proof generation but is not installed. " +
|
|
21
|
+
"It is an optional dependency of @zbase-protocol/core — run `npm i snarkjs` to enable generateWithdrawalProof/verifyProofLocally.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate a Groth16 withdrawal proof.
|
|
26
|
+
* Chain-agnostic: works for both EVM and Solana verifiers.
|
|
27
|
+
*/
|
|
28
|
+
export async function generateWithdrawalProof(input, wasmPath, zkeyPath) {
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
const snarkjs = await loadSnarkjs();
|
|
31
|
+
const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath);
|
|
32
|
+
return {
|
|
33
|
+
proof: proof,
|
|
34
|
+
publicSignals: publicSignals,
|
|
35
|
+
proofTimeMs: Date.now() - startTime,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Verify a Groth16 proof locally (for testing, not on-chain verification).
|
|
40
|
+
*/
|
|
41
|
+
export async function verifyProofLocally(proof, publicSignals, vkeyPath) {
|
|
42
|
+
const snarkjs = await loadSnarkjs();
|
|
43
|
+
const vkey = await fetch(vkeyPath).then((r) => r.json());
|
|
44
|
+
return snarkjs.groth16.verify(vkey, publicSignals, proof);
|
|
45
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zbase-protocol/core — ERC-5564 stealth addresses (scheme id 1, secp256k1)
|
|
3
|
+
*
|
|
4
|
+
* Implements the canonical ERC-5564 scheme 1 ("secp256k1-1") as deployed by
|
|
5
|
+
* ScopeLift / Umbra / Fluidkey, the same scheme audited by Trail of Bits.
|
|
6
|
+
* Spec: https://eips.ethereum.org/EIPS/eip-5564
|
|
7
|
+
*
|
|
8
|
+
* The scheme:
|
|
9
|
+
* - Provider holds two secp256k1 keypairs: spending (k_s, K_s) and viewing (k_v, K_v).
|
|
10
|
+
* - Meta-address encodes the two compressed public keys as
|
|
11
|
+
* st:<chain>:0x<spendingPubKey:33B><viewingPubKey:33B> // 132 hex chars
|
|
12
|
+
* - For each payment the facilitator (sender) generates a fresh ephemeral
|
|
13
|
+
* keypair (r, R = r·G), computes the ECDH shared point S = r · K_v,
|
|
14
|
+
* hashes it: s_h = keccak256(S_compressed). The view tag is s_h[0].
|
|
15
|
+
* - The stealth public key is P = K_s + s_h · G. The stealth Ethereum
|
|
16
|
+
* address is the standard keccak256(uncompressed-pub-key-without-prefix)[12:].
|
|
17
|
+
* - The provider scans Announcement events, recomputes S = k_v · R for
|
|
18
|
+
* each ephemeral pubkey, checks the view tag, and (on match) derives the
|
|
19
|
+
* stealth private key as k = (k_s + s_h) mod n.
|
|
20
|
+
*
|
|
21
|
+
* Why this matters: today every payment to OpenAI's x402 endpoint pays the
|
|
22
|
+
* same address; an outside observer maps "wallet X pays Nansen every 60s".
|
|
23
|
+
* After this ship, every payment to the same provider lands on a different
|
|
24
|
+
* stealth address derived from their registered meta-address — the buyer
|
|
25
|
+
* sees a single endpoint, the chain sees N unrelated recipients.
|
|
26
|
+
*
|
|
27
|
+
* Security boundary: the viewing private key never leaves the provider.
|
|
28
|
+
* An attacker who controls the facilitator AND knows the viewing key can
|
|
29
|
+
* correlate ephemeralPubkey → stealth recipient. Without the viewing key,
|
|
30
|
+
* the link is computationally hidden by the discrete-log assumption on
|
|
31
|
+
* secp256k1 (same assumption Ethereum signatures rely on).
|
|
32
|
+
*/
|
|
33
|
+
/** ERC-5564 scheme identifier. Only scheme 1 (secp256k1 + keccak256) is supported. */
|
|
34
|
+
export declare const STEALTH_SCHEME_ID: 1;
|
|
35
|
+
/** Default chain tag used in meta-address URIs ("st:<chain>:0x..."). */
|
|
36
|
+
export declare const DEFAULT_CHAIN_TAG = "base";
|
|
37
|
+
export interface StealthMetaAddress {
|
|
38
|
+
/** "st:base:0x<spendingPubKey:33B><viewingPubKey:33B>" */
|
|
39
|
+
metaAddress: string;
|
|
40
|
+
/** Hex-encoded compressed public key (66 hex chars, with 0x prefix). */
|
|
41
|
+
spendingPublicKey: string;
|
|
42
|
+
/** Hex-encoded compressed public key (66 hex chars, with 0x prefix). */
|
|
43
|
+
viewingPublicKey: string;
|
|
44
|
+
}
|
|
45
|
+
export interface StealthMetaAddressWithPrivate extends StealthMetaAddress {
|
|
46
|
+
/** Provider-only. Required to compute stealth private keys; NEVER share. */
|
|
47
|
+
spendingPrivateKey: string;
|
|
48
|
+
/** Provider-only. Required to scan; can be delegated to a view-only scanner. */
|
|
49
|
+
viewingPrivateKey: string;
|
|
50
|
+
}
|
|
51
|
+
export interface DerivedStealthAddress {
|
|
52
|
+
/** 0x-prefixed 20-byte Ethereum address. */
|
|
53
|
+
stealthAddress: string;
|
|
54
|
+
/** Hex-encoded compressed ephemeral public key R = r·G (66 hex chars). */
|
|
55
|
+
ephemeralPublicKey: string;
|
|
56
|
+
/** 1 byte (2 hex chars), most-significant byte of the hashed shared secret. */
|
|
57
|
+
viewTag: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ScanMatch {
|
|
60
|
+
/** Index into the input `recentEphemeralPubkeys` array. */
|
|
61
|
+
index: number;
|
|
62
|
+
/** The ephemeral pubkey that produced this match (echoed back for convenience). */
|
|
63
|
+
ephemeralPublicKey: string;
|
|
64
|
+
/** The stealth Ethereum address derived from this ephemeral pubkey. */
|
|
65
|
+
stealthAddress: string;
|
|
66
|
+
/** The view tag observed for this ephemeral pubkey. */
|
|
67
|
+
viewTag: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Produce a fresh ERC-5564 meta-address from an optional seed.
|
|
71
|
+
*
|
|
72
|
+
* The seed is intentionally optional: passing a deterministic seed enables
|
|
73
|
+
* reproducible tests; omitting it pulls 64 bytes of OS randomness. Either
|
|
74
|
+
* way, both keys live for the life of the provider — they are NOT rotated
|
|
75
|
+
* per call.
|
|
76
|
+
*
|
|
77
|
+
* @param seed Optional 64-byte Uint8Array. First 32 bytes seed the spending
|
|
78
|
+
* key, last 32 bytes seed the viewing key. If omitted, secure random.
|
|
79
|
+
*/
|
|
80
|
+
export declare function generateMetaAddress(seed?: Uint8Array, chainTag?: string): StealthMetaAddressWithPrivate;
|
|
81
|
+
/**
|
|
82
|
+
* Parse a "st:<chain>:0x<spending:33B><viewing:33B>" meta-address URI.
|
|
83
|
+
* Accepts either the full URI or a bare 0x-prefixed 132-hex-char string.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseMetaAddress(metaAddress: string): {
|
|
86
|
+
chainTag: string;
|
|
87
|
+
spendingPublicKey: Uint8Array;
|
|
88
|
+
viewingPublicKey: Uint8Array;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Quick sniff test: is this string an ERC-5564 meta-address (vs a plain
|
|
92
|
+
* 0x-address)? Used by the facilitator to decide which path to take.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isStealthMetaAddress(s: unknown): s is string;
|
|
95
|
+
/**
|
|
96
|
+
* Derive a fresh stealth address for a provider, given their meta-address
|
|
97
|
+
* and a 32-byte ephemeral nonce (= ephemeral private key r).
|
|
98
|
+
*
|
|
99
|
+
* The facilitator publishes `ephemeralPublicKey` on-chain alongside the
|
|
100
|
+
* payment; the provider scans for it and recovers the funds. The nonce
|
|
101
|
+
* MUST be unpredictable to anyone but the facilitator and MUST NOT repeat
|
|
102
|
+
* (a repeat collapses two payments to the same stealth address, leaking
|
|
103
|
+
* the link). Pass `undefined` to generate a fresh CSPRNG nonce.
|
|
104
|
+
*
|
|
105
|
+
* @param metaAddress provider's "st:base:0x..." meta-address
|
|
106
|
+
* @param ephemeralNonce 32-byte scalar, or undefined for fresh randomness
|
|
107
|
+
*/
|
|
108
|
+
export declare function deriveStealthAddress(metaAddress: string, ephemeralNonce?: Uint8Array): DerivedStealthAddress;
|
|
109
|
+
/**
|
|
110
|
+
* Given the provider's viewing private key and a batch of recent ephemeral
|
|
111
|
+
* pubkeys (from on-chain Announcement events or facilitator settle logs),
|
|
112
|
+
* return the stealth addresses that belong to this provider.
|
|
113
|
+
*
|
|
114
|
+
* View-tag optimization: if `expectedViewTags` is supplied, each candidate
|
|
115
|
+
* is filtered on its 1-byte view tag before the (more expensive) point add.
|
|
116
|
+
* This reduces scan cost by ~256× when most announcements aren't ours.
|
|
117
|
+
*
|
|
118
|
+
* @param viewingPrivateKey provider's viewing private key (32 bytes or hex)
|
|
119
|
+
* @param recentEphemeralPubkeys array of compressed ephemeral pubkeys
|
|
120
|
+
* @param spendingPublicKey provider's spending public key (needed to derive
|
|
121
|
+
* the stealth address from the shared secret)
|
|
122
|
+
* @param expectedViewTags optional array (same length as recentEphemeralPubkeys);
|
|
123
|
+
* if a tag is provided and doesn't match, the entry is skipped without
|
|
124
|
+
* doing the point arithmetic
|
|
125
|
+
*/
|
|
126
|
+
export declare function scanForPayments(viewingPrivateKey: Uint8Array | string, recentEphemeralPubkeys: Array<Uint8Array | string>, spendingPublicKey: Uint8Array | string, expectedViewTags?: Array<Uint8Array | string | undefined>): ScanMatch[];
|
|
127
|
+
/**
|
|
128
|
+
* Given the provider's spending+viewing private keys and an ephemeral pubkey
|
|
129
|
+
* that's known to belong to this provider, derive the stealth address's
|
|
130
|
+
* private key. With this key the provider can sweep funds to a treasury.
|
|
131
|
+
*
|
|
132
|
+
* k_stealth = (k_s + keccak256(k_v · R)) mod n
|
|
133
|
+
*
|
|
134
|
+
* @returns 32-byte stealth private key (raw bytes; 0x-encode if you need hex)
|
|
135
|
+
*/
|
|
136
|
+
export declare function computeStealthPrivateKey(spendingPrivateKey: Uint8Array | string, viewingPrivateKey: Uint8Array | string, ephemeralPublicKey: Uint8Array | string): Uint8Array;
|