@utxopia/sdk 0.1.0-alpha.4 → 0.1.0-alpha.6
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/dist/bitcoin/ika.d.ts +4 -3
- package/dist/bitcoin/ika.js +4 -7
- package/dist/client.d.ts +3 -3
- package/dist/client.js +3 -3
- package/dist/config.js +5 -2
- package/dist/crypto-babyjub.d.ts +0 -4
- package/dist/crypto-babyjub.js +0 -4
- package/dist/crypto.d.ts +2 -5
- package/dist/crypto.js +5 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/instructions.d.ts +0 -113
- package/dist/instructions.js +0 -127
- package/dist/poseidon.d.ts +0 -4
- package/dist/poseidon.js +0 -8
- package/dist/prover/index.d.ts +0 -1
- package/dist/prover/web.d.ts +8 -15
- package/dist/prover/web.js +24 -49
- package/dist/psbt.d.ts +2 -1
- package/dist/psbt.js +6 -2
- package/dist/spend-doc.js +1 -1
- package/dist/stealth.d.ts +6 -12
- package/dist/stealth.js +0 -14
- package/dist/taproot.d.ts +32 -4
- package/dist/taproot.js +54 -32
- package/dist/vk-registry.d.ts +21 -0
- package/dist/vk-registry.js +39 -0
- package/package.json +1 -32
- package/src/bitcoin/ika.ts +6 -11
- package/src/client.ts +7 -7
- package/src/config.ts +5 -2
- package/src/crypto-babyjub.ts +0 -5
- package/src/crypto.ts +5 -24
- package/src/index.ts +5 -0
- package/src/instructions.ts +0 -214
- package/src/poseidon.ts +0 -9
- package/src/prover/index.ts +0 -8
- package/src/prover/web.ts +26 -72
- package/src/psbt.ts +8 -3
- package/src/spend-doc.ts +1 -2
- package/src/stealth.ts +7 -23
- package/src/taproot.ts +74 -39
- package/src/vk-registry.ts +52 -0
package/src/stealth.ts
CHANGED
|
@@ -401,21 +401,21 @@ export interface NonInteractiveDepositWithRefundResult extends NonInteractiveDep
|
|
|
401
401
|
export async function createNonInteractiveDeposit(
|
|
402
402
|
recipientMeta: StealthMetaAddress,
|
|
403
403
|
custodyInternalKey: Uint8Array,
|
|
404
|
-
network?:
|
|
404
|
+
network?: BitcoinNetwork,
|
|
405
405
|
userRefundPubkey?: undefined,
|
|
406
406
|
opReturnContext?: DepositOpReturnContext,
|
|
407
407
|
): Promise<NonInteractiveDepositResult>;
|
|
408
408
|
export async function createNonInteractiveDeposit(
|
|
409
409
|
recipientMeta: StealthMetaAddress,
|
|
410
410
|
custodyInternalKey: Uint8Array,
|
|
411
|
-
network:
|
|
411
|
+
network: BitcoinNetwork,
|
|
412
412
|
userRefundPubkey: Uint8Array,
|
|
413
413
|
opReturnContext: DepositOpReturnContext,
|
|
414
414
|
): Promise<NonInteractiveDepositWithRefundResult>;
|
|
415
415
|
export async function createNonInteractiveDeposit(
|
|
416
416
|
recipientMeta: StealthMetaAddress,
|
|
417
417
|
custodyInternalKey: Uint8Array,
|
|
418
|
-
network:
|
|
418
|
+
network: BitcoinNetwork = "testnet",
|
|
419
419
|
userRefundPubkey?: Uint8Array,
|
|
420
420
|
opReturnContext?: DepositOpReturnContext,
|
|
421
421
|
): Promise<NonInteractiveDepositResult | NonInteractiveDepositWithRefundResult> {
|
|
@@ -491,7 +491,7 @@ export async function createNonInteractiveDeposit(
|
|
|
491
491
|
export async function createDirectVaultDeposit(
|
|
492
492
|
recipientMeta: StealthMetaAddress,
|
|
493
493
|
vaultXOnlyPubkey: Uint8Array,
|
|
494
|
-
network:
|
|
494
|
+
network: BitcoinNetwork = "testnet",
|
|
495
495
|
opReturnContext?: DepositOpReturnContext,
|
|
496
496
|
): Promise<NonInteractiveDepositResult> {
|
|
497
497
|
if (!opReturnContext) {
|
|
@@ -735,7 +735,7 @@ export async function createTweakDeposit(
|
|
|
735
735
|
recipientMeta: StealthMetaAddress,
|
|
736
736
|
vaultXOnlyPubkey: Uint8Array,
|
|
737
737
|
recovery: DepositRecoveryMaterial,
|
|
738
|
-
network:
|
|
738
|
+
network: BitcoinNetwork = "testnet",
|
|
739
739
|
): Promise<TweakDepositResult> {
|
|
740
740
|
if (vaultXOnlyPubkey.length !== 32) {
|
|
741
741
|
throw new Error("vaultXOnlyPubkey must be 32 bytes");
|
|
@@ -781,7 +781,7 @@ export async function createTweakDeposit(
|
|
|
781
781
|
*/
|
|
782
782
|
export async function createDepositFromConfig(
|
|
783
783
|
recipientMeta: StealthMetaAddress,
|
|
784
|
-
network:
|
|
784
|
+
network: BitcoinNetwork = "testnet",
|
|
785
785
|
opReturnContext?: DepositOpReturnContext,
|
|
786
786
|
): Promise<NonInteractiveDepositResult> {
|
|
787
787
|
const config = getConfig();
|
|
@@ -1220,6 +1220,7 @@ export async function scanUnifiedNotes(
|
|
|
1220
1220
|
// ========== Connection Adapter ==========
|
|
1221
1221
|
|
|
1222
1222
|
import type { Address } from "@solana/kit";
|
|
1223
|
+
import type { BitcoinNetwork } from "./taproot";
|
|
1223
1224
|
|
|
1224
1225
|
export interface ConnectionAdapter {
|
|
1225
1226
|
getAccountInfo: (
|
|
@@ -1285,23 +1286,6 @@ export function packStealthOutputForCircuit(output: StealthOutputData): CircuitS
|
|
|
1285
1286
|
};
|
|
1286
1287
|
}
|
|
1287
1288
|
|
|
1288
|
-
/**
|
|
1289
|
-
* Unpack encrypted amount from packed Field element
|
|
1290
|
-
*/
|
|
1291
|
-
export function unpackEncryptedAmountWithSign(packed: bigint): { encryptedAmount: Uint8Array; ySign: boolean } {
|
|
1292
|
-
const ySign = (packed & (1n << 64n)) !== 0n;
|
|
1293
|
-
const amount = packed & ((1n << 64n) - 1n);
|
|
1294
|
-
|
|
1295
|
-
const encryptedAmount = new Uint8Array(8);
|
|
1296
|
-
let temp = amount;
|
|
1297
|
-
for (let i = 0; i < 8; i++) {
|
|
1298
|
-
encryptedAmount[i] = Number(temp & 0xffn);
|
|
1299
|
-
temp >>= 8n;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
return { encryptedAmount, ySign };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1289
|
/**
|
|
1306
1290
|
* Create stealth output data for a self-send (change output)
|
|
1307
1291
|
*/
|
package/src/taproot.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
10
10
|
import { taggedHash, hexToBytes, bytesToHex } from "./crypto";
|
|
11
|
-
import
|
|
11
|
+
import { bech32, bech32m } from "@scure/base";
|
|
12
12
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
13
|
|
|
14
14
|
// Never use the secp256k1 generator as a custody key. Its discrete log is
|
|
@@ -31,7 +31,7 @@ const UNSAFE_GENERATOR_INTERNAL_KEY_HEX =
|
|
|
31
31
|
*/
|
|
32
32
|
export function deriveTaprootAddress(
|
|
33
33
|
commitment: Uint8Array,
|
|
34
|
-
network:
|
|
34
|
+
network: BitcoinNetwork = "testnet",
|
|
35
35
|
internalKey?: Uint8Array
|
|
36
36
|
): {
|
|
37
37
|
address: string;
|
|
@@ -77,11 +77,7 @@ export function deriveTaprootAddress(
|
|
|
77
77
|
const outputKeyHex = outputPoint.toHex(true); // 33-byte compressed hex
|
|
78
78
|
const outputKey = hexToBytes(outputKeyHex.slice(2)); // drop "02"/"03" prefix
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
82
|
-
const words = bech32.bech32m.toWords(outputKey);
|
|
83
|
-
// Witness version 1 for taproot
|
|
84
|
-
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
80
|
+
const address = p2trAddress(outputKey, network);
|
|
85
81
|
|
|
86
82
|
return {
|
|
87
83
|
address,
|
|
@@ -164,7 +160,7 @@ export function depositLeafScript(
|
|
|
164
160
|
export function deriveDepositAddress(
|
|
165
161
|
commitment: Uint8Array,
|
|
166
162
|
ikaXOnlyPubkey: Uint8Array,
|
|
167
|
-
network:
|
|
163
|
+
network: BitcoinNetwork = "testnet"
|
|
168
164
|
): {
|
|
169
165
|
address: string;
|
|
170
166
|
outputKey: Uint8Array;
|
|
@@ -203,25 +199,73 @@ export function verifyTaprootAddress(
|
|
|
203
199
|
internalKey?: Uint8Array
|
|
204
200
|
): boolean {
|
|
205
201
|
try {
|
|
206
|
-
const decoded =
|
|
202
|
+
const decoded = bech32m.decode(address as `${string}1${string}`);
|
|
207
203
|
const witnessVersion = decoded.words[0];
|
|
208
204
|
if (witnessVersion !== 1) {
|
|
209
205
|
return false;
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
);
|
|
208
|
+
const network = networkForHrp(decoded.prefix);
|
|
209
|
+
if (!network) return false;
|
|
215
210
|
|
|
216
|
-
const network = decoded.prefix === "bc" ? "mainnet" : "testnet";
|
|
217
211
|
const expected = deriveTaprootAddress(commitment, network, internalKey);
|
|
218
212
|
|
|
219
|
-
|
|
213
|
+
// Compare the whole address, not just the output key: the key is identical
|
|
214
|
+
// on every network, so an output-key match said nothing about the encoding.
|
|
215
|
+
// bech32 is case-insensitive, hence the fold.
|
|
216
|
+
return address.toLowerCase() === expected.address;
|
|
220
217
|
} catch {
|
|
221
218
|
return false;
|
|
222
219
|
}
|
|
223
220
|
}
|
|
224
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The networks whose addresses this SDK derives.
|
|
224
|
+
*
|
|
225
|
+
* Kept as one alias because the members drifted when it was spelled out at each
|
|
226
|
+
* call site: psbt.ts folded regtest into testnet while taproot.ts did not, and
|
|
227
|
+
* the two disagreed about what a bcrt1 address was.
|
|
228
|
+
*/
|
|
229
|
+
export type BitcoinNetwork = "mainnet" | "testnet" | "signet" | "regtest";
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* bech32 human-readable prefix for a Bitcoin network.
|
|
233
|
+
*
|
|
234
|
+
* Regtest is the reason this exists: it shares testnet's version bytes but not
|
|
235
|
+
* its prefix, and every place that folded the two together produced addresses
|
|
236
|
+
* no regtest node would accept.
|
|
237
|
+
*/
|
|
238
|
+
export function bech32Hrp(network: BitcoinNetwork): string {
|
|
239
|
+
return network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The network an address's bech32 prefix names, or null if it names none.
|
|
244
|
+
*
|
|
245
|
+
* Null rather than a testnet default: an unrecognised prefix is an address this
|
|
246
|
+
* SDK cannot place, and quietly calling it testnet is how bcrt1 addresses ended
|
|
247
|
+
* up classified as testnet everywhere.
|
|
248
|
+
*/
|
|
249
|
+
export function networkForHrp(prefix: string): BitcoinNetwork | null {
|
|
250
|
+
return prefix === "bc"
|
|
251
|
+
? "mainnet"
|
|
252
|
+
: prefix === "tb"
|
|
253
|
+
? "testnet"
|
|
254
|
+
: prefix === "bcrt"
|
|
255
|
+
? "regtest"
|
|
256
|
+
: null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Encode an x-only output key as a witness-v1 (P2TR) address.
|
|
261
|
+
*/
|
|
262
|
+
export function p2trAddress(
|
|
263
|
+
outputKey: Uint8Array,
|
|
264
|
+
network: BitcoinNetwork,
|
|
265
|
+
): string {
|
|
266
|
+
return bech32m.encode(bech32Hrp(network), [1, ...bech32m.toWords(outputKey)]);
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
/**
|
|
226
270
|
* Generate a P2TR (Pay-to-Taproot) script pubkey
|
|
227
271
|
*
|
|
@@ -264,18 +308,19 @@ export function parseP2TRScriptPubkey(
|
|
|
264
308
|
export function isValidBitcoinAddress(address: string): {
|
|
265
309
|
valid: boolean;
|
|
266
310
|
type: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr" | "unknown";
|
|
267
|
-
network:
|
|
311
|
+
network: BitcoinNetwork | "unknown";
|
|
268
312
|
} {
|
|
269
313
|
try {
|
|
270
314
|
// Bech32m (Taproot)
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
315
|
+
if (
|
|
316
|
+
address.startsWith("bc1p") ||
|
|
317
|
+
address.startsWith("tb1p") ||
|
|
318
|
+
address.startsWith("bcrt1p")
|
|
319
|
+
) {
|
|
320
|
+
const decoded = bech32m.decode(address as `${string}1${string}`);
|
|
321
|
+
const network = networkForHrp(decoded.prefix);
|
|
322
|
+
if (network && decoded.words[0] === 1 && decoded.words.length === 53) {
|
|
323
|
+
return { valid: true, type: "p2tr", network };
|
|
279
324
|
}
|
|
280
325
|
}
|
|
281
326
|
|
|
@@ -285,19 +330,11 @@ export function isValidBitcoinAddress(address: string): {
|
|
|
285
330
|
address.startsWith("tb1q") ||
|
|
286
331
|
address.startsWith("bcrt1q")
|
|
287
332
|
) {
|
|
288
|
-
const decoded = bech32.
|
|
289
|
-
|
|
333
|
+
const decoded = bech32.decode(address as `${string}1${string}`);
|
|
334
|
+
const network = networkForHrp(decoded.prefix);
|
|
335
|
+
if (network && decoded.words[0] === 0) {
|
|
290
336
|
const type = decoded.words.length === 33 ? "p2wpkh" : "p2wsh";
|
|
291
|
-
return {
|
|
292
|
-
valid: true,
|
|
293
|
-
type,
|
|
294
|
-
network:
|
|
295
|
-
decoded.prefix === "bc"
|
|
296
|
-
? "mainnet"
|
|
297
|
-
: decoded.prefix === "bcrt"
|
|
298
|
-
? "testnet"
|
|
299
|
-
: "testnet",
|
|
300
|
-
};
|
|
337
|
+
return { valid: true, type, network };
|
|
301
338
|
}
|
|
302
339
|
}
|
|
303
340
|
|
|
@@ -623,7 +660,7 @@ export function deriveTaprootAddressWithRefund(
|
|
|
623
660
|
npk: Uint8Array,
|
|
624
661
|
userRefundPubkey: Uint8Array,
|
|
625
662
|
internalKey: Uint8Array,
|
|
626
|
-
network:
|
|
663
|
+
network: BitcoinNetwork = "testnet"
|
|
627
664
|
): {
|
|
628
665
|
address: string;
|
|
629
666
|
outputKey: Uint8Array;
|
|
@@ -672,9 +709,7 @@ export function deriveTaprootAddressWithRefund(
|
|
|
672
709
|
controlBlock.set(internalKey, 1);
|
|
673
710
|
|
|
674
711
|
// 7. Encode as bech32m address
|
|
675
|
-
const
|
|
676
|
-
const words = bech32.bech32m.toWords(outputKey);
|
|
677
|
-
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
712
|
+
const address = p2trAddress(outputKey, network);
|
|
678
713
|
|
|
679
714
|
return {
|
|
680
715
|
address,
|
package/src/vk-registry.ts
CHANGED
|
@@ -293,3 +293,55 @@ export function isVkRegistryReady(
|
|
|
293
293
|
return false;
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Assert the circuit artifacts this client loaded correspond to the verifying key the chain
|
|
299
|
+
* will check against.
|
|
300
|
+
*
|
|
301
|
+
* `assertVkRegistryForShape` proves the registry exists and names the right shape;
|
|
302
|
+
* `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
|
|
303
|
+
* ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
|
|
304
|
+
* proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
|
|
305
|
+
* "proof invalid" — with nothing pointing at the version mismatch.
|
|
306
|
+
*
|
|
307
|
+
* It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
|
|
308
|
+
* `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
|
|
309
|
+
* ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
|
|
310
|
+
* recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
|
|
311
|
+
* foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
|
|
312
|
+
*
|
|
313
|
+
* @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
|
|
314
|
+
* it from anywhere else makes this assertion meaningless.
|
|
315
|
+
* @param registryData Raw `VkRegistry` account data for the shape.
|
|
316
|
+
*/
|
|
317
|
+
export function assertVkeyMatchesRegistry(
|
|
318
|
+
vkey: SnarkjsVkeyJson,
|
|
319
|
+
registryData: Uint8Array | null | undefined,
|
|
320
|
+
nInputs: number,
|
|
321
|
+
nOutputs: number,
|
|
322
|
+
): void {
|
|
323
|
+
const registry = assertVkRegistryForShape(registryData, nInputs, nOutputs);
|
|
324
|
+
|
|
325
|
+
const expectedIc = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
|
|
326
|
+
if (vkey.IC.length !== expectedIc) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Circuit vkey for JoinSplit ${nInputs}x${nOutputs} has ${vkey.IC.length} IC points, ` +
|
|
329
|
+
`expected ${expectedIc} — the artifacts are for a different shape.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const local = computeVkHash(vkey);
|
|
334
|
+
if (!local.every((b, i) => b === registry.vkHash[i])) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Circuit artifacts for JoinSplit ${nInputs}x${nOutputs} do not match the on-chain ` +
|
|
337
|
+
`verifying key (artifact vkHash ${bytesToHex(local)}, registry ${bytesToHex(registry.vkHash)}). ` +
|
|
338
|
+
`A proof built from these would be rejected as invalid. Usually the circuit base URL ` +
|
|
339
|
+
`points at a stale build — check it resolves to the same version the registry was ` +
|
|
340
|
+
`registered from.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function bytesToHex(b: Uint8Array): string {
|
|
346
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
347
|
+
}
|