@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/dist/prover/web.js
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* - Signature = EdDSA-Poseidon over (merkleRoot, boundParamsHash, nullifiers..., commitmentsOut...)
|
|
11
11
|
*/
|
|
12
12
|
import { poseidonHashSync, computeJoinSplitCommitmentSync, computeJoinSplitNullifierSync, } from "../poseidon";
|
|
13
|
-
import { BN254_FIELD_PRIME } from "../crypto";
|
|
13
|
+
import { BN254_FIELD_PRIME, bytesToHex } from "../crypto";
|
|
14
14
|
import { TREE_DEPTH } from "../merkle";
|
|
15
|
-
import { getConfig } from "../config";
|
|
16
15
|
import { MAX_SAFE_JOINSPLIT_SIZE } from "../vk-registry";
|
|
17
16
|
/** Maximum satoshis (total BTC supply) */
|
|
18
17
|
const MAX_SATOSHIS = 21000000n * 100000000n;
|
|
@@ -42,6 +41,26 @@ const isBrowser = typeof window !== "undefined";
|
|
|
42
41
|
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
43
42
|
// Configurable circuit paths
|
|
44
43
|
let circuitBasePath = isBrowser ? "/circuits/groth16" : "./circuits";
|
|
44
|
+
/**
|
|
45
|
+
* Fetch a circuit's `*.vkey.json` from the same base path its zkey comes from.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately reads `circuitBasePath` rather than taking a URL: the point of the check this
|
|
48
|
+
* feeds (`assertVkeyMatchesRegistry`) is that the vkey describes the artifacts actually being
|
|
49
|
+
* proved with, and a caller-supplied URL could describe a different build entirely.
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchCircuitVkey(circuitType) {
|
|
52
|
+
const cached = vkeyCache.get(circuitType);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
const url = `${circuitBasePath}/${circuitType}/${circuitType}.vkey.json`;
|
|
56
|
+
const res = await fetch(url);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`Could not fetch circuit vkey ${url}: ${res.status} ${res.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
const vkey = await res.json();
|
|
61
|
+
vkeyCache.set(circuitType, vkey);
|
|
62
|
+
return vkey;
|
|
63
|
+
}
|
|
45
64
|
/**
|
|
46
65
|
* Set the base path for circuit artifacts
|
|
47
66
|
*/
|
|
@@ -50,18 +69,14 @@ export function setCircuitPath(path) {
|
|
|
50
69
|
return;
|
|
51
70
|
circuitBasePath = path;
|
|
52
71
|
circuitCache.clear();
|
|
72
|
+
vkeyCache.clear();
|
|
53
73
|
artifactBytesCache.clear();
|
|
54
74
|
artifactDownloadCache.clear();
|
|
55
75
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Get the current circuit base path
|
|
58
|
-
*/
|
|
59
|
-
export function getCircuitPath() {
|
|
60
|
-
return circuitBasePath;
|
|
61
|
-
}
|
|
62
76
|
// Lazy-loaded snarkjs module
|
|
63
77
|
let snarkjs = null;
|
|
64
78
|
const circuitCache = new Map();
|
|
79
|
+
const vkeyCache = new Map();
|
|
65
80
|
let proverInitialized = false;
|
|
66
81
|
/**
|
|
67
82
|
* Load snarkjs module
|
|
@@ -115,7 +130,6 @@ export function setCircuitArtifactDigests(digests) {
|
|
|
115
130
|
// Anything admitted under the old policy must be re-checked under the new one.
|
|
116
131
|
artifactBytesCache.clear();
|
|
117
132
|
}
|
|
118
|
-
const toHex = (buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
119
133
|
async function verifyArtifactBytes(url, bytes) {
|
|
120
134
|
if (!artifactDigests)
|
|
121
135
|
return;
|
|
@@ -125,7 +139,7 @@ async function verifyArtifactBytes(url, bytes) {
|
|
|
125
139
|
throw new Error(`No integrity digest recorded for circuit artifact ${url} — refusing to prove. ` +
|
|
126
140
|
`Regenerate the artifact manifest so it covers every shape this origin serves.`);
|
|
127
141
|
}
|
|
128
|
-
const actual =
|
|
142
|
+
const actual = bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength))));
|
|
129
143
|
if (actual !== artifactDigests[key]) {
|
|
130
144
|
throw new Error(`Circuit artifact ${key} failed its integrity check (expected ${artifactDigests[key]}, ` +
|
|
131
145
|
`got ${actual}). Refusing to prove against an artifact this build does not recognise.`);
|
|
@@ -556,42 +570,3 @@ export async function cleanup() {
|
|
|
556
570
|
// ==========================================================================
|
|
557
571
|
// Solana Instruction Building (for Groth16 on-chain verification)
|
|
558
572
|
// ==========================================================================
|
|
559
|
-
/**
|
|
560
|
-
* Groth16 verifier program ID (from current config)
|
|
561
|
-
*/
|
|
562
|
-
export function getGroth16VerifierProgramId() {
|
|
563
|
-
const config = getConfig();
|
|
564
|
-
return config.groth16VerifierProgramId;
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Build instruction data for Groth16 verification
|
|
568
|
-
*/
|
|
569
|
-
export function buildVerifyInstructionData(proof, publicSignals, vkHash) {
|
|
570
|
-
const piBytes = publicSignals.flatMap((pi) => {
|
|
571
|
-
const bytes = new Array(32).fill(0);
|
|
572
|
-
const bigint = BigInt(pi);
|
|
573
|
-
for (let i = 31; i >= 0; i--) {
|
|
574
|
-
bytes[i] = Number((bigint >> BigInt((31 - i) * 8)) & 0xffn);
|
|
575
|
-
}
|
|
576
|
-
return bytes;
|
|
577
|
-
});
|
|
578
|
-
const cleanHex = vkHash.startsWith("0x") ? vkHash.slice(2) : vkHash;
|
|
579
|
-
const vkHashBytes = new Uint8Array(cleanHex.length / 2);
|
|
580
|
-
for (let i = 0; i < vkHashBytes.length; i++) {
|
|
581
|
-
vkHashBytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
|
|
582
|
-
}
|
|
583
|
-
const totalSize = proof.length + 4 + piBytes.length + 32;
|
|
584
|
-
const data = new Uint8Array(totalSize);
|
|
585
|
-
let offset = 0;
|
|
586
|
-
data.set(proof, offset);
|
|
587
|
-
offset += proof.length;
|
|
588
|
-
const piCount = publicSignals.length;
|
|
589
|
-
data[offset++] = piCount & 0xff;
|
|
590
|
-
data[offset++] = (piCount >> 8) & 0xff;
|
|
591
|
-
data[offset++] = (piCount >> 16) & 0xff;
|
|
592
|
-
data[offset++] = (piCount >> 24) & 0xff;
|
|
593
|
-
data.set(new Uint8Array(piBytes), offset);
|
|
594
|
-
offset += piBytes.length;
|
|
595
|
-
data.set(vkHashBytes, offset);
|
|
596
|
-
return data;
|
|
597
|
-
}
|
package/dist/psbt.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Uses @scure/btc-signer for PSBT construction.
|
|
11
11
|
*/
|
|
12
|
+
import { type BitcoinNetwork } from "./taproot";
|
|
12
13
|
/** UTXO descriptor for PSBT inputs */
|
|
13
14
|
export interface UtxoDescriptor {
|
|
14
15
|
/** Transaction ID (hex, 64 chars) */
|
|
@@ -44,7 +45,7 @@ export interface BuildDepositPsbtParams {
|
|
|
44
45
|
/** Fee rate in sats/vbyte */
|
|
45
46
|
feeRate: number;
|
|
46
47
|
/** Bitcoin network */
|
|
47
|
-
network?:
|
|
48
|
+
network?: BitcoinNetwork;
|
|
48
49
|
}
|
|
49
50
|
/** Result of PSBT construction */
|
|
50
51
|
export interface BuildDepositPsbtResult {
|
package/dist/psbt.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as btc from "@scure/btc-signer";
|
|
13
13
|
import { hex } from "@scure/base";
|
|
14
|
-
import { DEPOSIT_OP_RETURN_SIZE, createOpReturnScriptFromPayload } from "./taproot";
|
|
14
|
+
import { DEPOSIT_OP_RETURN_SIZE, createOpReturnScriptFromPayload, bech32Hrp } from "./taproot";
|
|
15
15
|
// =============================================================================
|
|
16
16
|
// Constants
|
|
17
17
|
// =============================================================================
|
|
@@ -64,7 +64,11 @@ export function buildDepositPsbt(params) {
|
|
|
64
64
|
if (opReturnPayload && opReturnPayload.length !== DEPOSIT_OP_RETURN_SIZE) {
|
|
65
65
|
throw new Error(`OP_RETURN payload must be ${DEPOSIT_OP_RETURN_SIZE} bytes, got ${opReturnPayload.length}`);
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// Regtest shares testnet's version bytes but not its bech32 hrp, so folding it
|
|
68
|
+
// into TEST_NETWORK leaves every bcrt1 address undecodable.
|
|
69
|
+
const btcNetwork = network === "mainnet"
|
|
70
|
+
? btc.NETWORK
|
|
71
|
+
: { ...btc.TEST_NETWORK, bech32: bech32Hrp(network) };
|
|
68
72
|
// Calculate total input value
|
|
69
73
|
const totalInput = senderUtxos.reduce((sum, u) => sum + u.value, 0);
|
|
70
74
|
// Detect input type from first UTXO
|
package/dist/spend-doc.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Poseidon(merkleRoot, boundParamsHash, nullifiers.., commitmentsOut..).
|
|
12
12
|
*/
|
|
13
13
|
import { computeSolanaDomainBoundParamsHash, createRedeemBoundParams, createTransferBoundParams, createUnshieldBoundParams, } from "./bound-params";
|
|
14
|
+
import { bytesToHex as hex } from "./crypto";
|
|
14
15
|
export class SpendDocMismatch extends Error {
|
|
15
16
|
}
|
|
16
17
|
function fmt(raw, decimals) {
|
|
@@ -20,7 +21,6 @@ function fmt(raw, decimals) {
|
|
|
20
21
|
const frac = decimals === 0 ? "" : s.slice(s.length - decimals).replace(/0+$/, "");
|
|
21
22
|
return `${neg ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
|
|
22
23
|
}
|
|
23
|
-
const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
24
24
|
function sortedValues(v) {
|
|
25
25
|
return [...v].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(",");
|
|
26
26
|
}
|
package/dist/stealth.d.ts
CHANGED
|
@@ -189,8 +189,8 @@ export interface NonInteractiveDepositWithRefundResult extends NonInteractiveDep
|
|
|
189
189
|
* @param network - Bitcoin network for address encoding
|
|
190
190
|
* @param userRefundPubkey - Optional 32-byte x-only pubkey for refund script path
|
|
191
191
|
*/
|
|
192
|
-
export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAddress, custodyInternalKey: Uint8Array, network?:
|
|
193
|
-
export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAddress, custodyInternalKey: Uint8Array, network:
|
|
192
|
+
export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAddress, custodyInternalKey: Uint8Array, network?: BitcoinNetwork, userRefundPubkey?: undefined, opReturnContext?: DepositOpReturnContext): Promise<NonInteractiveDepositResult>;
|
|
193
|
+
export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAddress, custodyInternalKey: Uint8Array, network: BitcoinNetwork, userRefundPubkey: Uint8Array, opReturnContext: DepositOpReturnContext): Promise<NonInteractiveDepositWithRefundResult>;
|
|
194
194
|
/**
|
|
195
195
|
* Create a non-interactive deposit directly to an Ika-controlled vault.
|
|
196
196
|
*
|
|
@@ -199,7 +199,7 @@ export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAd
|
|
|
199
199
|
* in OP_RETURN(header || poolTag || ephemeralPub || npk), and the destination chain
|
|
200
200
|
* credits the note from that transaction.
|
|
201
201
|
*/
|
|
202
|
-
export declare function createDirectVaultDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?:
|
|
202
|
+
export declare function createDirectVaultDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?: BitcoinNetwork, opReturnContext?: DepositOpReturnContext): Promise<NonInteractiveDepositResult>;
|
|
203
203
|
/**
|
|
204
204
|
* The delegable half of deposit recovery.
|
|
205
205
|
*
|
|
@@ -322,7 +322,7 @@ export interface TweakDepositResult {
|
|
|
322
322
|
* with no OP_RETURN is invisible to block scanning, so an unregistered address
|
|
323
323
|
* is one nobody is watching.
|
|
324
324
|
*/
|
|
325
|
-
export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, recovery: DepositRecoveryMaterial, network?:
|
|
325
|
+
export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, recovery: DepositRecoveryMaterial, network?: BitcoinNetwork): Promise<TweakDepositResult>;
|
|
326
326
|
/**
|
|
327
327
|
* Create a non-interactive deposit using the current SDK config.
|
|
328
328
|
*
|
|
@@ -333,7 +333,7 @@ export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, va
|
|
|
333
333
|
* credits the note by SPV-verifying that deposit transaction directly. Legacy sweep-mode
|
|
334
334
|
* address derivation is intentionally not selected from config anymore.
|
|
335
335
|
*/
|
|
336
|
-
export declare function createDepositFromConfig(recipientMeta: StealthMetaAddress, network?:
|
|
336
|
+
export declare function createDepositFromConfig(recipientMeta: StealthMetaAddress, network?: BitcoinNetwork, opReturnContext?: DepositOpReturnContext): Promise<NonInteractiveDepositResult>;
|
|
337
337
|
export declare function isDirectVaultDepositMode(mode?: string): boolean;
|
|
338
338
|
/**
|
|
339
339
|
* Choose the Taproot internal key for deposit-address derivation.
|
|
@@ -448,6 +448,7 @@ export declare function scanUnifiedNotesMulti(source: WalletSignerAdapter | UTXO
|
|
|
448
448
|
*/
|
|
449
449
|
export declare function scanUnifiedNotes(source: WalletSignerAdapter | UTXOpiaKeys, announcements: OnChainStealthAnnouncement[], tokenId: bigint): Promise<ScannedNote[]>;
|
|
450
450
|
import type { Address } from "@solana/kit";
|
|
451
|
+
import type { BitcoinNetwork } from "./taproot";
|
|
451
452
|
export interface ConnectionAdapter {
|
|
452
453
|
getAccountInfo: (pubkey: Address) => Promise<{
|
|
453
454
|
data: Uint8Array;
|
|
@@ -480,13 +481,6 @@ export declare function packEncryptedAmountWithSign(encryptedAmount: Uint8Array,
|
|
|
480
481
|
* Convert StealthOutputData to circuit-ready format
|
|
481
482
|
*/
|
|
482
483
|
export declare function packStealthOutputForCircuit(output: StealthOutputData): CircuitStealthOutput;
|
|
483
|
-
/**
|
|
484
|
-
* Unpack encrypted amount from packed Field element
|
|
485
|
-
*/
|
|
486
|
-
export declare function unpackEncryptedAmountWithSign(packed: bigint): {
|
|
487
|
-
encryptedAmount: Uint8Array;
|
|
488
|
-
ySign: boolean;
|
|
489
|
-
};
|
|
490
484
|
/**
|
|
491
485
|
* Create stealth output data for a self-send (change output)
|
|
492
486
|
*/
|
package/dist/stealth.js
CHANGED
|
@@ -737,20 +737,6 @@ export function packStealthOutputForCircuit(output) {
|
|
|
737
737
|
encryptedAmountWithSign,
|
|
738
738
|
};
|
|
739
739
|
}
|
|
740
|
-
/**
|
|
741
|
-
* Unpack encrypted amount from packed Field element
|
|
742
|
-
*/
|
|
743
|
-
export function unpackEncryptedAmountWithSign(packed) {
|
|
744
|
-
const ySign = (packed & (1n << 64n)) !== 0n;
|
|
745
|
-
const amount = packed & ((1n << 64n) - 1n);
|
|
746
|
-
const encryptedAmount = new Uint8Array(8);
|
|
747
|
-
let temp = amount;
|
|
748
|
-
for (let i = 0; i < 8; i++) {
|
|
749
|
-
encryptedAmount[i] = Number(temp & 0xffn);
|
|
750
|
-
temp >>= 8n;
|
|
751
|
-
}
|
|
752
|
-
return { encryptedAmount, ySign };
|
|
753
|
-
}
|
|
754
740
|
/**
|
|
755
741
|
* Create stealth output data for a self-send (change output)
|
|
756
742
|
*/
|
package/dist/taproot.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @param internalKey - Configured FROST/Ika custody key (x-only, 32 bytes; required)
|
|
19
19
|
* @returns Taproot address (bc1p... or tb1p...)
|
|
20
20
|
*/
|
|
21
|
-
export declare function deriveTaprootAddress(commitment: Uint8Array, network?:
|
|
21
|
+
export declare function deriveTaprootAddress(commitment: Uint8Array, network?: BitcoinNetwork, internalKey?: Uint8Array): {
|
|
22
22
|
address: string;
|
|
23
23
|
outputKey: Uint8Array;
|
|
24
24
|
tweak: Uint8Array;
|
|
@@ -64,7 +64,7 @@ export declare function depositLeafScript(commitment: Uint8Array, ikaXOnlyPubkey
|
|
|
64
64
|
* Returns everything the sweeper needs to spend it: a script-path witness is
|
|
65
65
|
* `[signature, leafScript, controlBlock]`.
|
|
66
66
|
*/
|
|
67
|
-
export declare function deriveDepositAddress(commitment: Uint8Array, ikaXOnlyPubkey: Uint8Array, network?:
|
|
67
|
+
export declare function deriveDepositAddress(commitment: Uint8Array, ikaXOnlyPubkey: Uint8Array, network?: BitcoinNetwork): {
|
|
68
68
|
address: string;
|
|
69
69
|
outputKey: Uint8Array;
|
|
70
70
|
leafScript: Uint8Array;
|
|
@@ -80,6 +80,34 @@ export declare function deriveDepositAddress(commitment: Uint8Array, ikaXOnlyPub
|
|
|
80
80
|
* @returns true if address matches expected derivation
|
|
81
81
|
*/
|
|
82
82
|
export declare function verifyTaprootAddress(address: string, commitment: Uint8Array, internalKey?: Uint8Array): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The networks whose addresses this SDK derives.
|
|
85
|
+
*
|
|
86
|
+
* Kept as one alias because the members drifted when it was spelled out at each
|
|
87
|
+
* call site: psbt.ts folded regtest into testnet while taproot.ts did not, and
|
|
88
|
+
* the two disagreed about what a bcrt1 address was.
|
|
89
|
+
*/
|
|
90
|
+
export type BitcoinNetwork = "mainnet" | "testnet" | "signet" | "regtest";
|
|
91
|
+
/**
|
|
92
|
+
* bech32 human-readable prefix for a Bitcoin network.
|
|
93
|
+
*
|
|
94
|
+
* Regtest is the reason this exists: it shares testnet's version bytes but not
|
|
95
|
+
* its prefix, and every place that folded the two together produced addresses
|
|
96
|
+
* no regtest node would accept.
|
|
97
|
+
*/
|
|
98
|
+
export declare function bech32Hrp(network: BitcoinNetwork): string;
|
|
99
|
+
/**
|
|
100
|
+
* The network an address's bech32 prefix names, or null if it names none.
|
|
101
|
+
*
|
|
102
|
+
* Null rather than a testnet default: an unrecognised prefix is an address this
|
|
103
|
+
* SDK cannot place, and quietly calling it testnet is how bcrt1 addresses ended
|
|
104
|
+
* up classified as testnet everywhere.
|
|
105
|
+
*/
|
|
106
|
+
export declare function networkForHrp(prefix: string): BitcoinNetwork | null;
|
|
107
|
+
/**
|
|
108
|
+
* Encode an x-only output key as a witness-v1 (P2TR) address.
|
|
109
|
+
*/
|
|
110
|
+
export declare function p2trAddress(outputKey: Uint8Array, network: BitcoinNetwork): string;
|
|
83
111
|
/**
|
|
84
112
|
* Generate a P2TR (Pay-to-Taproot) script pubkey
|
|
85
113
|
*
|
|
@@ -100,7 +128,7 @@ export declare function parseP2TRScriptPubkey(scriptPubkey: Uint8Array): Uint8Ar
|
|
|
100
128
|
export declare function isValidBitcoinAddress(address: string): {
|
|
101
129
|
valid: boolean;
|
|
102
130
|
type: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr" | "unknown";
|
|
103
|
-
network:
|
|
131
|
+
network: BitcoinNetwork | "unknown";
|
|
104
132
|
};
|
|
105
133
|
/** Destination chain encoded in the compact deposit OP_RETURN header. */
|
|
106
134
|
export declare const DEPOSIT_DESTINATION_CHAIN: {
|
|
@@ -201,7 +229,7 @@ export declare function computeTapLeafHash(script: Uint8Array, leafVersion?: num
|
|
|
201
229
|
* @param internalKey - 32-byte x-only FROST group public key
|
|
202
230
|
* @param network - Bitcoin network for address encoding
|
|
203
231
|
*/
|
|
204
|
-
export declare function deriveTaprootAddressWithRefund(npk: Uint8Array, userRefundPubkey: Uint8Array, internalKey: Uint8Array, network?:
|
|
232
|
+
export declare function deriveTaprootAddressWithRefund(npk: Uint8Array, userRefundPubkey: Uint8Array, internalKey: Uint8Array, network?: BitcoinNetwork): {
|
|
205
233
|
address: string;
|
|
206
234
|
outputKey: Uint8Array;
|
|
207
235
|
merkleRoot: Uint8Array;
|
package/dist/taproot.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9
9
|
import { taggedHash, hexToBytes, bytesToHex } from "./crypto";
|
|
10
|
-
import
|
|
10
|
+
import { bech32, bech32m } from "@scure/base";
|
|
11
11
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
12
12
|
// Never use the secp256k1 generator as a custody key. Its discrete log is
|
|
13
13
|
// public, so a key-path output derived from it is sweepable by anyone.
|
|
@@ -59,11 +59,7 @@ export function deriveTaprootAddress(commitment, network = "testnet", internalKe
|
|
|
59
59
|
// x-only output key (BIP-340): drop prefix from compressed form
|
|
60
60
|
const outputKeyHex = outputPoint.toHex(true); // 33-byte compressed hex
|
|
61
61
|
const outputKey = hexToBytes(outputKeyHex.slice(2)); // drop "02"/"03" prefix
|
|
62
|
-
|
|
63
|
-
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
64
|
-
const words = bech32.bech32m.toWords(outputKey);
|
|
65
|
-
// Witness version 1 for taproot
|
|
66
|
-
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
62
|
+
const address = p2trAddress(outputKey, network);
|
|
67
63
|
return {
|
|
68
64
|
address,
|
|
69
65
|
outputKey,
|
|
@@ -151,20 +147,56 @@ export function deriveDepositAddress(commitment, ikaXOnlyPubkey, network = "test
|
|
|
151
147
|
*/
|
|
152
148
|
export function verifyTaprootAddress(address, commitment, internalKey) {
|
|
153
149
|
try {
|
|
154
|
-
const decoded =
|
|
150
|
+
const decoded = bech32m.decode(address);
|
|
155
151
|
const witnessVersion = decoded.words[0];
|
|
156
152
|
if (witnessVersion !== 1) {
|
|
157
153
|
return false;
|
|
158
154
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
155
|
+
const network = networkForHrp(decoded.prefix);
|
|
156
|
+
if (!network)
|
|
157
|
+
return false;
|
|
161
158
|
const expected = deriveTaprootAddress(commitment, network, internalKey);
|
|
162
|
-
|
|
159
|
+
// Compare the whole address, not just the output key: the key is identical
|
|
160
|
+
// on every network, so an output-key match said nothing about the encoding.
|
|
161
|
+
// bech32 is case-insensitive, hence the fold.
|
|
162
|
+
return address.toLowerCase() === expected.address;
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
165
165
|
return false;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* bech32 human-readable prefix for a Bitcoin network.
|
|
170
|
+
*
|
|
171
|
+
* Regtest is the reason this exists: it shares testnet's version bytes but not
|
|
172
|
+
* its prefix, and every place that folded the two together produced addresses
|
|
173
|
+
* no regtest node would accept.
|
|
174
|
+
*/
|
|
175
|
+
export function bech32Hrp(network) {
|
|
176
|
+
return network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The network an address's bech32 prefix names, or null if it names none.
|
|
180
|
+
*
|
|
181
|
+
* Null rather than a testnet default: an unrecognised prefix is an address this
|
|
182
|
+
* SDK cannot place, and quietly calling it testnet is how bcrt1 addresses ended
|
|
183
|
+
* up classified as testnet everywhere.
|
|
184
|
+
*/
|
|
185
|
+
export function networkForHrp(prefix) {
|
|
186
|
+
return prefix === "bc"
|
|
187
|
+
? "mainnet"
|
|
188
|
+
: prefix === "tb"
|
|
189
|
+
? "testnet"
|
|
190
|
+
: prefix === "bcrt"
|
|
191
|
+
? "regtest"
|
|
192
|
+
: null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Encode an x-only output key as a witness-v1 (P2TR) address.
|
|
196
|
+
*/
|
|
197
|
+
export function p2trAddress(outputKey, network) {
|
|
198
|
+
return bech32m.encode(bech32Hrp(network), [1, ...bech32m.toWords(outputKey)]);
|
|
199
|
+
}
|
|
168
200
|
/**
|
|
169
201
|
* Generate a P2TR (Pay-to-Taproot) script pubkey
|
|
170
202
|
*
|
|
@@ -203,32 +235,24 @@ export function parseP2TRScriptPubkey(scriptPubkey) {
|
|
|
203
235
|
export function isValidBitcoinAddress(address) {
|
|
204
236
|
try {
|
|
205
237
|
// Bech32m (Taproot)
|
|
206
|
-
if (address.startsWith("bc1p") ||
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
238
|
+
if (address.startsWith("bc1p") ||
|
|
239
|
+
address.startsWith("tb1p") ||
|
|
240
|
+
address.startsWith("bcrt1p")) {
|
|
241
|
+
const decoded = bech32m.decode(address);
|
|
242
|
+
const network = networkForHrp(decoded.prefix);
|
|
243
|
+
if (network && decoded.words[0] === 1 && decoded.words.length === 53) {
|
|
244
|
+
return { valid: true, type: "p2tr", network };
|
|
214
245
|
}
|
|
215
246
|
}
|
|
216
247
|
// Bech32 (SegWit v0)
|
|
217
248
|
if (address.startsWith("bc1q") ||
|
|
218
249
|
address.startsWith("tb1q") ||
|
|
219
250
|
address.startsWith("bcrt1q")) {
|
|
220
|
-
const decoded = bech32.
|
|
221
|
-
|
|
251
|
+
const decoded = bech32.decode(address);
|
|
252
|
+
const network = networkForHrp(decoded.prefix);
|
|
253
|
+
if (network && decoded.words[0] === 0) {
|
|
222
254
|
const type = decoded.words.length === 33 ? "p2wpkh" : "p2wsh";
|
|
223
|
-
return {
|
|
224
|
-
valid: true,
|
|
225
|
-
type,
|
|
226
|
-
network: decoded.prefix === "bc"
|
|
227
|
-
? "mainnet"
|
|
228
|
-
: decoded.prefix === "bcrt"
|
|
229
|
-
? "testnet"
|
|
230
|
-
: "testnet",
|
|
231
|
-
};
|
|
255
|
+
return { valid: true, type, network };
|
|
232
256
|
}
|
|
233
257
|
}
|
|
234
258
|
// Legacy (base58check)
|
|
@@ -533,9 +557,7 @@ export function deriveTaprootAddressWithRefund(npk, userRefundPubkey, internalKe
|
|
|
533
557
|
controlBlock[0] = 0xc0 | parityBit;
|
|
534
558
|
controlBlock.set(internalKey, 1);
|
|
535
559
|
// 7. Encode as bech32m address
|
|
536
|
-
const
|
|
537
|
-
const words = bech32.bech32m.toWords(outputKey);
|
|
538
|
-
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
560
|
+
const address = p2trAddress(outputKey, network);
|
|
539
561
|
return {
|
|
540
562
|
address,
|
|
541
563
|
outputKey,
|
package/dist/vk-registry.d.ts
CHANGED
|
@@ -98,3 +98,24 @@ export declare function parseVkRegistry(data: Uint8Array): ParsedVkRegistry;
|
|
|
98
98
|
export declare function assertVkRegistryForShape(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): ParsedVkRegistry;
|
|
99
99
|
/** True if `data` is an initialized `VkRegistry` for the given shape. */
|
|
100
100
|
export declare function isVkRegistryReady(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Assert the circuit artifacts this client loaded correspond to the verifying key the chain
|
|
103
|
+
* will check against.
|
|
104
|
+
*
|
|
105
|
+
* `assertVkRegistryForShape` proves the registry exists and names the right shape;
|
|
106
|
+
* `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
|
|
107
|
+
* ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
|
|
108
|
+
* proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
|
|
109
|
+
* "proof invalid" — with nothing pointing at the version mismatch.
|
|
110
|
+
*
|
|
111
|
+
* It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
|
|
112
|
+
* `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
|
|
113
|
+
* ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
|
|
114
|
+
* recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
|
|
115
|
+
* foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
|
|
116
|
+
*
|
|
117
|
+
* @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
|
|
118
|
+
* it from anywhere else makes this assertion meaningless.
|
|
119
|
+
* @param registryData Raw `VkRegistry` account data for the shape.
|
|
120
|
+
*/
|
|
121
|
+
export declare function assertVkeyMatchesRegistry(vkey: SnarkjsVkeyJson, registryData: Uint8Array | null | undefined, nInputs: number, nOutputs: number): void;
|
package/dist/vk-registry.js
CHANGED
|
@@ -210,3 +210,42 @@ export function isVkRegistryReady(data, nInputs, nOutputs) {
|
|
|
210
210
|
return false;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Assert the circuit artifacts this client loaded correspond to the verifying key the chain
|
|
215
|
+
* will check against.
|
|
216
|
+
*
|
|
217
|
+
* `assertVkRegistryForShape` proves the registry exists and names the right shape;
|
|
218
|
+
* `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
|
|
219
|
+
* ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
|
|
220
|
+
* proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
|
|
221
|
+
* "proof invalid" — with nothing pointing at the version mismatch.
|
|
222
|
+
*
|
|
223
|
+
* It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
|
|
224
|
+
* `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
|
|
225
|
+
* ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
|
|
226
|
+
* recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
|
|
227
|
+
* foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
|
|
228
|
+
*
|
|
229
|
+
* @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
|
|
230
|
+
* it from anywhere else makes this assertion meaningless.
|
|
231
|
+
* @param registryData Raw `VkRegistry` account data for the shape.
|
|
232
|
+
*/
|
|
233
|
+
export function assertVkeyMatchesRegistry(vkey, registryData, nInputs, nOutputs) {
|
|
234
|
+
const registry = assertVkRegistryForShape(registryData, nInputs, nOutputs);
|
|
235
|
+
const expectedIc = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
|
|
236
|
+
if (vkey.IC.length !== expectedIc) {
|
|
237
|
+
throw new Error(`Circuit vkey for JoinSplit ${nInputs}x${nOutputs} has ${vkey.IC.length} IC points, ` +
|
|
238
|
+
`expected ${expectedIc} — the artifacts are for a different shape.`);
|
|
239
|
+
}
|
|
240
|
+
const local = computeVkHash(vkey);
|
|
241
|
+
if (!local.every((b, i) => b === registry.vkHash[i])) {
|
|
242
|
+
throw new Error(`Circuit artifacts for JoinSplit ${nInputs}x${nOutputs} do not match the on-chain ` +
|
|
243
|
+
`verifying key (artifact vkHash ${bytesToHex(local)}, registry ${bytesToHex(registry.vkHash)}). ` +
|
|
244
|
+
`A proof built from these would be rejected as invalid. Usually the circuit base URL ` +
|
|
245
|
+
`points at a stale build — check it resolves to the same version the registry was ` +
|
|
246
|
+
`registered from.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function bytesToHex(b) {
|
|
250
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utxopia/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"description": "UTXOpia SDK - Private Bitcoin across Solana and Sui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,40 +27,10 @@
|
|
|
27
27
|
"import": "./dist/prover/mobile.js",
|
|
28
28
|
"default": "./dist/prover/mobile.js"
|
|
29
29
|
},
|
|
30
|
-
"./stealth": {
|
|
31
|
-
"types": "./dist/stealth/index.d.ts",
|
|
32
|
-
"import": "./dist/stealth/index.js",
|
|
33
|
-
"default": "./dist/stealth/index.js"
|
|
34
|
-
},
|
|
35
30
|
"./bitcoin": {
|
|
36
31
|
"types": "./dist/bitcoin/index.d.ts",
|
|
37
32
|
"import": "./dist/bitcoin/index.js",
|
|
38
33
|
"default": "./dist/bitcoin/index.js"
|
|
39
|
-
},
|
|
40
|
-
"./solana": {
|
|
41
|
-
"types": "./dist/solana/index.d.ts",
|
|
42
|
-
"import": "./dist/solana/index.js",
|
|
43
|
-
"default": "./dist/solana/index.js"
|
|
44
|
-
},
|
|
45
|
-
"./watcher": {
|
|
46
|
-
"types": "./dist/watcher/index.d.ts",
|
|
47
|
-
"import": "./dist/watcher/index.js",
|
|
48
|
-
"default": "./dist/watcher/index.js"
|
|
49
|
-
},
|
|
50
|
-
"./watcher/web": {
|
|
51
|
-
"types": "./dist/watcher/web.d.ts",
|
|
52
|
-
"import": "./dist/watcher/web.js",
|
|
53
|
-
"default": "./dist/watcher/web.js"
|
|
54
|
-
},
|
|
55
|
-
"./watcher/native": {
|
|
56
|
-
"types": "./dist/watcher/native.d.ts",
|
|
57
|
-
"import": "./dist/watcher/native.js",
|
|
58
|
-
"default": "./dist/watcher/native.js"
|
|
59
|
-
},
|
|
60
|
-
"./react": {
|
|
61
|
-
"types": "./dist/react/index.d.ts",
|
|
62
|
-
"import": "./dist/react/index.js",
|
|
63
|
-
"default": "./dist/react/index.js"
|
|
64
34
|
}
|
|
65
35
|
},
|
|
66
36
|
"files": [
|
|
@@ -93,7 +63,6 @@
|
|
|
93
63
|
"@scure/btc-signer": "2.0.1",
|
|
94
64
|
"@solana-program/system": "^0.10.0",
|
|
95
65
|
"@solana/kit": "^5.5.0",
|
|
96
|
-
"bech32": "^2.0.0",
|
|
97
66
|
"circomlibjs": "^0.1.7",
|
|
98
67
|
"poseidon-lite": "0.3.0"
|
|
99
68
|
},
|
package/src/bitcoin/ika.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { taggedHash, hexToBytes, bytesToHex } from "../crypto";
|
|
11
|
-
import {
|
|
11
|
+
import { p2trAddress, type BitcoinNetwork } from "../taproot";
|
|
12
12
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -33,12 +33,12 @@ export type IkaDWalletRef =
|
|
|
33
33
|
* address = bech32m(hrp, [witness_version=1, ...words(output_key)])
|
|
34
34
|
*
|
|
35
35
|
* @param ref The Ika dWallet reference (literal pubkey, or future async id).
|
|
36
|
-
* @param network
|
|
36
|
+
* @param network BitcoinNetwork
|
|
37
37
|
* @returns The P2TR address (`bc1p…` / `tb1p…` / `bcrt1p…`)
|
|
38
38
|
*/
|
|
39
39
|
export function deriveCustodyAddressFromIkaDWallet(
|
|
40
40
|
ref: IkaDWalletRef,
|
|
41
|
-
network:
|
|
41
|
+
network: BitcoinNetwork
|
|
42
42
|
): string {
|
|
43
43
|
const xonly = extractXOnly(ref);
|
|
44
44
|
const tweak = taggedHash("TapTweak", xonly);
|
|
@@ -52,10 +52,7 @@ export function deriveCustodyAddressFromIkaDWallet(
|
|
|
52
52
|
// Drop the 1-byte parity prefix to get the x-only output key.
|
|
53
53
|
const outputKey = hexToBytes(outputPoint.toHex(true).slice(2));
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
57
|
-
const words = bech32m.toWords(outputKey);
|
|
58
|
-
return bech32m.encode(hrp, [1, ...words]);
|
|
55
|
+
return p2trAddress(outputKey, network);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
/**
|
|
@@ -67,14 +64,12 @@ export function deriveCustodyAddressFromIkaDWallet(
|
|
|
67
64
|
*/
|
|
68
65
|
export function deriveRawXOnlyP2TRAddress(
|
|
69
66
|
xonlyPubkey: Uint8Array,
|
|
70
|
-
network:
|
|
67
|
+
network: BitcoinNetwork
|
|
71
68
|
): string {
|
|
72
69
|
if (xonlyPubkey.length !== 32) {
|
|
73
70
|
throw new Error("xonlyPubkey must be 32 bytes");
|
|
74
71
|
}
|
|
75
|
-
|
|
76
|
-
network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
77
|
-
return bech32m.encode(hrp, [1, ...bech32m.toWords(xonlyPubkey)]);
|
|
72
|
+
return p2trAddress(xonlyPubkey, network);
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
function extractXOnly(ref: IkaDWalletRef): Uint8Array {
|