blindseal-sdk 0.5.0 → 0.5.1
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/index.d.ts +39 -5
- package/dist/index.js +62 -5
- package/dist/ix.d.ts +22 -3
- package/dist/ix.js +23 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Connection, Keypair, PublicKey, type Signer } from '@solana/web3.js';
|
|
2
|
-
import { type Attestation } from './ix.js';
|
|
2
|
+
import { type Attestation, type DecodedPublicInputs } from './ix.js';
|
|
3
3
|
export * from './ix.js';
|
|
4
4
|
export declare class WrongClusterError extends Error {
|
|
5
5
|
readonly got: string;
|
|
@@ -21,9 +21,43 @@ export interface SubmitOptions {
|
|
|
21
21
|
export declare function submit(att: Attestation, opts: SubmitOptions): Promise<string>;
|
|
22
22
|
/** Has this attestation's nullifier been consumed on chain? */
|
|
23
23
|
export declare function isConsumed(att: Attestation, conn: Connection, programId?: PublicKey): Promise<boolean>;
|
|
24
|
-
|
|
24
|
+
/** Labeled top-level fields that must equal the committed public inputs. */
|
|
25
|
+
export declare const LABELED_FIELDS: readonly ["payloadHash", "policyRoot", "detectorVersion", "actionHash", "nullifier", "expirySlot"];
|
|
26
|
+
export type LabeledField = (typeof LABELED_FIELDS)[number];
|
|
27
|
+
/**
|
|
28
|
+
* Result of `verifyOffchain`.
|
|
29
|
+
* - `ok`: true only if the Groth16 proof verifies AND every labeled field equals its decoded value.
|
|
30
|
+
* - `reason`: present when `ok` is false — the wasm verifier's reason if the proof failed, otherwise
|
|
31
|
+
* `label mismatch: <field>[, <field>…]` (or the decode error if the blob is not 168 bytes).
|
|
32
|
+
* - `decoded`: the six fields decoded from `publicInputs[0]`; present whenever the blob parses,
|
|
33
|
+
* even when the proof or the labels fail. Hex fields are 0x-prefixed lower-case; `expirySlot`
|
|
34
|
+
* is a decimal u64 string.
|
|
35
|
+
* - `mismatches`: the labeled fields that differ from `decoded` (empty when none, or when the blob
|
|
36
|
+
* did not parse).
|
|
37
|
+
*/
|
|
38
|
+
export interface VerifyOffchainResult {
|
|
25
39
|
ok: boolean;
|
|
26
|
-
reason
|
|
40
|
+
reason?: string;
|
|
41
|
+
decoded?: DecodedPublicInputs;
|
|
42
|
+
mismatches: string[];
|
|
27
43
|
}
|
|
28
|
-
/**
|
|
29
|
-
export
|
|
44
|
+
/** @deprecated Use `VerifyOffchainResult`. */
|
|
45
|
+
export type VerifyResult = VerifyOffchainResult;
|
|
46
|
+
/**
|
|
47
|
+
* Compare the six labeled top-level fields of an attestation to the decoded committed blob.
|
|
48
|
+
* Hex is compared case-insensitively with or without `0x`; `expirySlot` may be a number, bigint or
|
|
49
|
+
* decimal string. A missing or malformed label counts as a mismatch. Returns the differing field
|
|
50
|
+
* names in canonical order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function labelMismatches(att: Partial<Record<LabeledField, unknown>>, decoded: DecodedPublicInputs): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Off-chain verify via the same wasm the verify pages use, then cross-check the labels.
|
|
55
|
+
* 1. Decode `publicInputs[0]` into its six fields (see `decodePublicInputs`).
|
|
56
|
+
* 2. Groth16 verify of `proof.bytes` against `publicInputs[0]`, using the DECODED `detectorVersion`
|
|
57
|
+
* as the vkey hash (the committed one — same as the on-chain program, which checks the vkey it
|
|
58
|
+
* verified with equals the blob's). The proof therefore stands or falls on the blob alone.
|
|
59
|
+
* 3. Compare each labeled top-level field to the decoded value; any difference fails the result,
|
|
60
|
+
* because the proof binds only the blob — a tampered label would otherwise pass unnoticed.
|
|
61
|
+
* Does NOT check expiry against the current slot, nullifier consumption or the on-chain allowlists.
|
|
62
|
+
*/
|
|
63
|
+
export declare function verifyOffchain(att: Attestation): Promise<VerifyOffchainResult>;
|
package/dist/index.js
CHANGED
|
@@ -41,12 +41,69 @@ export async function isConsumed(att, conn, programId = PROGRAM_ID) {
|
|
|
41
41
|
const info = await conn.getAccountInfo(nullifierPda(nullifier, programId), 'confirmed');
|
|
42
42
|
return info !== null;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
44
|
+
/** Labeled top-level fields that must equal the committed public inputs. */
|
|
45
|
+
export const LABELED_FIELDS = ['payloadHash', 'policyRoot', 'detectorVersion', 'actionHash', 'nullifier', 'expirySlot'];
|
|
46
|
+
const normHex = (v) => '0x' + String(v ?? '').trim().replace(/^0x/i, '').toLowerCase();
|
|
47
|
+
const normSlot = (v) => {
|
|
48
|
+
if (typeof v === 'bigint')
|
|
49
|
+
return v.toString();
|
|
50
|
+
if (typeof v === 'number')
|
|
51
|
+
return Number.isInteger(v) && v >= 0 ? String(v) : null;
|
|
52
|
+
if (typeof v === 'string' && /^\d+$/.test(v.trim()))
|
|
53
|
+
return BigInt(v.trim()).toString();
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Compare the six labeled top-level fields of an attestation to the decoded committed blob.
|
|
58
|
+
* Hex is compared case-insensitively with or without `0x`; `expirySlot` may be a number, bigint or
|
|
59
|
+
* decimal string. A missing or malformed label counts as a mismatch. Returns the differing field
|
|
60
|
+
* names in canonical order.
|
|
61
|
+
*/
|
|
62
|
+
export function labelMismatches(att, decoded) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const k of LABELED_FIELDS) {
|
|
65
|
+
const same = k === 'expirySlot' ? normSlot(att.expirySlot) === decoded.expirySlot : normHex(att[k]) === decoded[k];
|
|
66
|
+
if (!same)
|
|
67
|
+
out.push(k);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
let wasmReady = null;
|
|
72
|
+
const loadWasm = () => (wasmReady ??= (async () => {
|
|
46
73
|
const wasm = await import('blindseal-verifier-wasm');
|
|
47
74
|
const { readFileSync } = await import('node:fs');
|
|
48
75
|
const req = createRequire(import.meta.url);
|
|
49
|
-
|
|
50
|
-
wasm
|
|
51
|
-
|
|
76
|
+
wasm.initSync({ module: readFileSync(req.resolve('blindseal-verifier-wasm/bg.wasm')) });
|
|
77
|
+
return wasm;
|
|
78
|
+
})().catch((e) => { wasmReady = null; throw e; }));
|
|
79
|
+
/**
|
|
80
|
+
* Off-chain verify via the same wasm the verify pages use, then cross-check the labels.
|
|
81
|
+
* 1. Decode `publicInputs[0]` into its six fields (see `decodePublicInputs`).
|
|
82
|
+
* 2. Groth16 verify of `proof.bytes` against `publicInputs[0]`, using the DECODED `detectorVersion`
|
|
83
|
+
* as the vkey hash (the committed one — same as the on-chain program, which checks the vkey it
|
|
84
|
+
* verified with equals the blob's). The proof therefore stands or falls on the blob alone.
|
|
85
|
+
* 3. Compare each labeled top-level field to the decoded value; any difference fails the result,
|
|
86
|
+
* because the proof binds only the blob — a tampered label would otherwise pass unnoticed.
|
|
87
|
+
* Does NOT check expiry against the current slot, nullifier consumption or the on-chain allowlists.
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyOffchain(att) {
|
|
90
|
+
const wasm = await loadWasm();
|
|
91
|
+
let decoded;
|
|
92
|
+
let decodeError;
|
|
93
|
+
try {
|
|
94
|
+
decoded = decodePublicInputs(String(att.publicInputs?.[0] ?? ''));
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
decodeError = e instanceof Error ? e.message : String(e);
|
|
98
|
+
}
|
|
99
|
+
const forWasm = decoded ? { ...att, detectorVersion: decoded.detectorVersion } : att;
|
|
100
|
+
const proof = JSON.parse(wasm.verify_attestation_json(JSON.stringify(forWasm)));
|
|
101
|
+
const mismatches = decoded ? labelMismatches(att, decoded) : [];
|
|
102
|
+
if (!proof.ok)
|
|
103
|
+
return { ok: false, reason: proof.reason ?? 'proof did not verify', decoded, mismatches };
|
|
104
|
+
if (!decoded)
|
|
105
|
+
return { ok: false, reason: decodeError ?? 'publicInputs did not decode', mismatches };
|
|
106
|
+
if (mismatches.length)
|
|
107
|
+
return { ok: false, reason: `label mismatch: ${mismatches.join(', ')}`, decoded, mismatches };
|
|
108
|
+
return { ok: true, decoded, mismatches };
|
|
52
109
|
}
|
package/dist/ix.d.ts
CHANGED
|
@@ -54,15 +54,34 @@ export declare const policyRootPda: (root: Uint8Array, programId?: PublicKey) =>
|
|
|
54
54
|
export declare const nullifierPda: (nullifier: Uint8Array, programId?: PublicKey) => PublicKey;
|
|
55
55
|
export declare function computeActionHash(a: ActionArgs, payloadHash: Uint8Array, policyRoot: Uint8Array, detectorVersion: Uint8Array, expirySlot: bigint): Buffer;
|
|
56
56
|
export declare const computeNullifier: (actionHash: Uint8Array) => Buffer;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
export declare const PUBLIC_INPUTS_LEN = 168;
|
|
58
|
+
/** The six committed fields as 0x-prefixed lower-case hex; `expirySlot` is a decimal u64 string. */
|
|
59
|
+
export interface DecodedPublicInputs {
|
|
60
|
+
payloadHash: string;
|
|
61
|
+
policyRoot: string;
|
|
62
|
+
detectorVersion: string;
|
|
63
|
+
actionHash: string;
|
|
64
|
+
nullifier: string;
|
|
65
|
+
expirySlot: string;
|
|
66
|
+
}
|
|
67
|
+
/** Byte-level view of the same six fields (used by the instruction builders). */
|
|
68
|
+
export interface DecodedPublicInputsBytes {
|
|
59
69
|
payloadHash: Buffer;
|
|
60
70
|
policyRoot: Buffer;
|
|
61
71
|
detectorVersion: Buffer;
|
|
62
72
|
actionHash: Buffer;
|
|
63
73
|
nullifier: Buffer;
|
|
64
74
|
expirySlot: bigint;
|
|
65
|
-
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decode the 168-byte committed blob into its six fields.
|
|
78
|
+
* - Given a hex string (`publicInputs[0]`, with or without `0x`), returns 0x-prefixed lower-case
|
|
79
|
+
* hex fields and `expirySlot` as a decimal string — the shape `verifyOffchain` reports as `decoded`.
|
|
80
|
+
* - Given raw bytes, returns Buffers and a bigint expiry (the form the instruction builders use).
|
|
81
|
+
* Throws if the input is not exactly 168 bytes (or not valid hex).
|
|
82
|
+
*/
|
|
83
|
+
export declare function decodePublicInputs(blobHex: string): DecodedPublicInputs;
|
|
84
|
+
export declare function decodePublicInputs(blob: Uint8Array): DecodedPublicInputsBytes;
|
|
66
85
|
export declare const discriminator: (name: string) => Buffer;
|
|
67
86
|
export declare function encodeActionArgs(a: ActionArgs): Buffer;
|
|
68
87
|
export interface VerifyAccounts {
|
package/dist/ix.js
CHANGED
|
@@ -43,10 +43,30 @@ export function computeActionHash(a, payloadHash, policyRoot, detectorVersion, e
|
|
|
43
43
|
.digest();
|
|
44
44
|
}
|
|
45
45
|
export const computeNullifier = (actionHash) => createHash('sha256').update(NULLIFIER_DOMAIN).update(actionHash).digest();
|
|
46
|
-
|
|
46
|
+
// ---- the committed public-input blob ----
|
|
47
|
+
// `publicInputs[0]` is the guest's public values in commit order (crates/guest/src/main.rs):
|
|
48
|
+
// [0..32) payloadHash [32..64) policyRoot [64..96) detectorVersion
|
|
49
|
+
// [96..128) actionHash [128..160) nullifier [160..168) expirySlot (u64 LE)
|
|
50
|
+
// 168 bytes = 336 hex chars. This is the ONLY data the proof binds; the six labeled top-level
|
|
51
|
+
// fields of attestation.json are convenience copies and must be checked against it.
|
|
52
|
+
export const PUBLIC_INPUTS_LEN = 168;
|
|
47
53
|
export function decodePublicInputs(blob) {
|
|
48
|
-
if (blob
|
|
49
|
-
|
|
54
|
+
if (typeof blob === 'string') {
|
|
55
|
+
const h = blob.trim().replace(/^0x/i, '').toLowerCase();
|
|
56
|
+
if (!/^[0-9a-f]*$/.test(h) || h.length % 2 !== 0)
|
|
57
|
+
throw new Error('publicInputs is not a hex string');
|
|
58
|
+
const b = decodeBytes(Buffer.from(h, 'hex'));
|
|
59
|
+
const hx = (x) => '0x' + x.toString('hex');
|
|
60
|
+
return {
|
|
61
|
+
payloadHash: hx(b.payloadHash), policyRoot: hx(b.policyRoot), detectorVersion: hx(b.detectorVersion),
|
|
62
|
+
actionHash: hx(b.actionHash), nullifier: hx(b.nullifier), expirySlot: b.expirySlot.toString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return decodeBytes(blob);
|
|
66
|
+
}
|
|
67
|
+
function decodeBytes(blob) {
|
|
68
|
+
if (blob.length !== PUBLIC_INPUTS_LEN)
|
|
69
|
+
throw new Error(`publicInputs must be ${PUBLIC_INPUTS_LEN} bytes, got ${blob.length}`);
|
|
50
70
|
const b = Buffer.from(blob);
|
|
51
71
|
return {
|
|
52
72
|
payloadHash: b.subarray(0, 32), policyRoot: b.subarray(32, 64), detectorVersion: b.subarray(64, 96),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blindseal-sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Blindseal SDK — build the on-chain verify instruction, submit attestations to Solana mainnet, verify attestations and disclosures off-chain.",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@solana/web3.js": "1.98.0",
|
|
22
|
-
"blindseal-verifier-wasm": "0.5.
|
|
22
|
+
"blindseal-verifier-wasm": "0.5.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"typescript": "5.6.3",
|