zk-credits 0.2.1 → 0.2.3
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/README.md +39 -28
- package/circuits/manifest.json +20 -3
- package/dist/artifact-bundle.d.ts +19 -0
- package/dist/artifact-bundle.js +61 -1
- package/dist/artifact-release.d.ts +10 -0
- package/dist/artifact-release.js +243 -0
- package/dist/cli.js +126 -22
- package/dist/setup-config.d.ts +38 -0
- package/dist/setup-config.js +357 -0
- package/dist/zk-credits.js +848 -161
- package/package.json +4 -1
- package/dist/setup-preflight.d.ts +0 -2
- package/dist/setup-preflight.js +0 -12
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ are founder-provisioned test credits, and the circuit is not independently
|
|
|
7
7
|
audited.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install --global zk-credits@
|
|
10
|
+
npm install --global zk-credits@0.2.3
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Pinned pilot versions
|
|
@@ -17,17 +17,17 @@ not mix versions between the sidecar, the adapter package, and the gateway:
|
|
|
17
17
|
|
|
18
18
|
| Package | Version |
|
|
19
19
|
| --- | --- |
|
|
20
|
-
| `zk-credits` (this sidecar) | `0.2.
|
|
20
|
+
| `zk-credits` (this sidecar) | `0.2.3` |
|
|
21
21
|
| `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
|
|
22
22
|
| `@zk-credits/shared` | `0.1.0` |
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
the
|
|
24
|
+
The pinned Base Sepolia trial version is `0.2.3`. It uses the non-streaming
|
|
25
|
+
`POST /v1/chat/completions` path below, includes the loopback Responses bridge
|
|
26
|
+
for Codex, and acquires the pinned private proving bundle through the local
|
|
27
|
+
GitHub CLI session. It also synchronizes V2 Base events and checks the gateway
|
|
28
|
+
known root before local setup completes. Neither this version nor the adapter
|
|
29
|
+
supports a Stellar or evaluation path. The proving artifacts are available
|
|
30
|
+
only from the private immutable release pinned by `circuits/manifest.json`.
|
|
31
31
|
|
|
32
32
|
Verify the shipped artifacts before your first prove. The package's
|
|
33
33
|
`circuits/manifest.json` fixes the SHA-256 of the frozen
|
|
@@ -57,33 +57,44 @@ still receives the class. A rejected request consumes no credit.
|
|
|
57
57
|
|
|
58
58
|
## First run
|
|
59
59
|
|
|
60
|
-
1. Redeem an invite and download the encrypted credential. Credits
|
|
61
|
-
founder-provisioned test credits; there is no payment step.
|
|
62
|
-
2.
|
|
63
|
-
bundle
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
|
|
60
|
+
1. Redeem an invite and download the encrypted V2 credential export. Credits
|
|
61
|
+
are founder-provisioned test credits; there is no payment step.
|
|
62
|
+
2. Ask the pilot contact for read access to the private bundle repository
|
|
63
|
+
`mangekyou-labs/zk-credits-base-sepolia-v2-bundle`, then authenticate the
|
|
64
|
+
GitHub CLI on this machine with `gh auth login`. The sidecar uses that local
|
|
65
|
+
GitHub CLI session to read the immutable release identified in
|
|
66
|
+
`circuits/manifest.json`; it never receives or stores your GitHub token.
|
|
67
|
+
Set the credential, gateway, and Base Sepolia RPC endpoint:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
|
|
71
|
-
export ZK_CREDITS_ARTIFACT_DIR="/path/to/pinned-bundle"
|
|
72
|
-
export ZK_CREDITS_WITNESS_PATH="/path/to/base-tree.json"
|
|
73
71
|
export ZK_CREDITS_GATEWAY_URL="https://zk-credits-gateway.onrender.com"
|
|
72
|
+
export BASE_RPC_URL="https://sepolia.base.org"
|
|
74
73
|
```
|
|
75
74
|
|
|
75
|
+
`zk-credits setup codex` searches the standard local folders first. If it
|
|
76
|
+
finds no valid bundle, it downloads the pinned release, verifies the
|
|
77
|
+
release id, tag, immutability, archive digest, and all three file digests,
|
|
78
|
+
then atomically installs the bundle under `~/.zk-credits/artifacts` with
|
|
79
|
+
owner-only permissions. Missing GitHub access or any mismatch stops setup.
|
|
80
|
+
It syncs public Base events from the pinned V2 bond and deployment block,
|
|
81
|
+
validates that the resulting witness resolves the activated credential,
|
|
82
|
+
and asks the gateway whether the resulting root is known. The witness and
|
|
83
|
+
event cache remain local. The package does not accept a supplied witness
|
|
84
|
+
file for this V2 deployment.
|
|
85
|
+
|
|
86
|
+
The package ships `circuits/manifest.json`, which fixes the immutable
|
|
87
|
+
release identity, the SHA-256 of the archive and each proving artifact, and
|
|
88
|
+
the V2 Base deployment pins. `BASE_PRIVATE_CREDIT_BOND_ADDRESS` and
|
|
89
|
+
`BASE_DEPLOYMENT_BLOCK` are optional checks; if set, they must match the
|
|
90
|
+
manifest. V2 setup rejects `ZK_CREDITS_WITNESS_PATH` because it builds the
|
|
91
|
+
witness from the pinned public Base events. The local setup config is
|
|
92
|
+
owner-readable only and never stores the credential password. If the RPC
|
|
93
|
+
URL contains a provider key, it remains in that local config file.
|
|
94
|
+
|
|
76
95
|
A missing, relocated, symlinked-out, or altered artifact fails closed
|
|
77
96
|
before the first prove. A hash mismatch is a proof failure: no payment
|
|
78
|
-
leaves, and the local slot stays reusable.
|
|
79
|
-
|
|
80
|
-
`ZK_CREDITS_WITNESS_PATH` points to a local witness artifact — either a
|
|
81
|
-
prepared witness (`root`, `pathElements`, `pathIndices`) or a public tree
|
|
82
|
-
(`leaves: [{ index, leaf, expiry? }]`, optional `root`) from which the
|
|
83
|
-
sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
|
|
84
|
-
`BASE_PRIVATE_CREDIT_BOND_ADDRESS`, and optionally
|
|
85
|
-
`BASE_DEPLOYMENT_BLOCK` so the sidecar synchronizes public `BundleFunded`
|
|
86
|
-
events and builds a local Merkle witness. The gateway never serves a path
|
|
97
|
+
leaves, and the local slot stays reusable. The gateway never serves a path
|
|
87
98
|
for a named leaf or commitment.
|
|
88
99
|
|
|
89
100
|
3. Start setup. It verifies the credential, pinned artifact hashes, and
|
package/circuits/manifest.json
CHANGED
|
@@ -16,13 +16,30 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"file": "private_credit_spend.zkey",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "7cc83e6df3f0c805d8b49d4063ee56a93123ff19acd49d639ff263588ef7a144"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"file": "verification_key_private_credit.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "4844820509c32722c0c867f028a713df8a1b6478ff7376a80861e7dcbc9c51fc"
|
|
24
24
|
}
|
|
25
25
|
],
|
|
26
|
+
"release": {
|
|
27
|
+
"repository": "mangekyou-labs/zk-credits-base-sepolia-v2-bundle",
|
|
28
|
+
"tag": "v2.0.0",
|
|
29
|
+
"releaseId": 397705951,
|
|
30
|
+
"assetName": "base-sepolia-v2.tar.gz",
|
|
31
|
+
"sha256": "dc60b3109b3d907e2b9e8c4253fb52e77400d763eb443968e8de3af6b7e0b709"
|
|
32
|
+
},
|
|
33
|
+
"deployment": {
|
|
34
|
+
"chainId": 84532,
|
|
35
|
+
"bondAddress": "0xcc1909dD30485d6D9a46e3d33cf04C1378b586Ab",
|
|
36
|
+
"deploymentBlock": "47372040",
|
|
37
|
+
"deploymentDomain": "84532",
|
|
38
|
+
"circuitId": "private-credit-spend-bn254-dev",
|
|
39
|
+
"verifyingKeyId": "private-credit-spend-vk-dev-sepolia-v2",
|
|
40
|
+
"spendVerifierAddress": "0x37663bC461AB8EccD3b7D69Bde7eeF13D489723e",
|
|
41
|
+
"groth16VerifierAddress": "0x5870117EA7ACb28B3f36fb64284b77c4eD2B7148"
|
|
42
|
+
},
|
|
26
43
|
"publicSignals": ["root", "timestamp", "domain", "requestSignal", "nullifier", "share"],
|
|
27
|
-
"provingMaterial": "Development-only Sepolia artifacts. Complete an external production ceremony before mainnet."
|
|
44
|
+
"provingMaterial": "Development-only Sepolia artifacts, with one local phase-2 contribution. Complete an external production ceremony before mainnet."
|
|
28
45
|
}
|
|
@@ -19,6 +19,23 @@ export interface PinnedArtifact {
|
|
|
19
19
|
file: string;
|
|
20
20
|
sha256: string;
|
|
21
21
|
}
|
|
22
|
+
export interface PinnedArtifactRelease {
|
|
23
|
+
repository: string;
|
|
24
|
+
tag: string;
|
|
25
|
+
releaseId: number;
|
|
26
|
+
assetName: string;
|
|
27
|
+
sha256: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PinnedDeployment {
|
|
30
|
+
chainId: number;
|
|
31
|
+
bondAddress: string;
|
|
32
|
+
deploymentBlock: string;
|
|
33
|
+
deploymentDomain: string;
|
|
34
|
+
circuitId: string;
|
|
35
|
+
verifyingKeyId: string;
|
|
36
|
+
spendVerifierAddress: string;
|
|
37
|
+
groth16VerifierAddress: string;
|
|
38
|
+
}
|
|
22
39
|
export interface CircuitManifest {
|
|
23
40
|
version: number;
|
|
24
41
|
scheme: string;
|
|
@@ -31,6 +48,8 @@ export interface CircuitManifest {
|
|
|
31
48
|
verificationKey: string;
|
|
32
49
|
};
|
|
33
50
|
artifacts: PinnedArtifact[];
|
|
51
|
+
release?: PinnedArtifactRelease;
|
|
52
|
+
deployment?: PinnedDeployment;
|
|
34
53
|
}
|
|
35
54
|
export interface PinnedArtifactBundle {
|
|
36
55
|
directory: string;
|
package/dist/artifact-bundle.js
CHANGED
|
@@ -26,6 +26,8 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
|
26
26
|
/** A pinned artifact is a bare file name, never a path or a remote location. */
|
|
27
27
|
const SAFE_FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
28
28
|
const REMOTE_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u;
|
|
29
|
+
const ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/u;
|
|
30
|
+
const RELEASE_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
29
31
|
function isRecord(value) {
|
|
30
32
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
31
33
|
}
|
|
@@ -51,6 +53,7 @@ export function parseCircuitManifest(value) {
|
|
|
51
53
|
if (!isRecord(value.circuit))
|
|
52
54
|
throw new ArtifactBundleError('artifact_manifest_malformed', 'Circuit manifest is missing the circuit block');
|
|
53
55
|
const circuit = value.circuit;
|
|
56
|
+
const network = requiredString(value, 'network');
|
|
54
57
|
const depth = circuit.depth;
|
|
55
58
|
if (!Number.isSafeInteger(depth) || depth <= 0) {
|
|
56
59
|
throw new ArtifactBundleError('artifact_manifest_malformed', 'Circuit depth must be a positive integer');
|
|
@@ -73,7 +76,7 @@ export function parseCircuitManifest(value) {
|
|
|
73
76
|
const resolved = {
|
|
74
77
|
version: 1,
|
|
75
78
|
scheme: requiredString(value, 'scheme'),
|
|
76
|
-
network
|
|
79
|
+
network,
|
|
77
80
|
circuit: {
|
|
78
81
|
id: requiredString(circuit, 'id'),
|
|
79
82
|
depth: depth,
|
|
@@ -82,7 +85,12 @@ export function parseCircuitManifest(value) {
|
|
|
82
85
|
verificationKey: pinnedFile(circuit.verificationKey, 'verificationKey'),
|
|
83
86
|
},
|
|
84
87
|
artifacts,
|
|
88
|
+
...(value.release === undefined ? {} : { release: parseRelease(value.release) }),
|
|
89
|
+
...(value.deployment === undefined ? {} : { deployment: parseDeployment(value.deployment, requiredString(circuit, 'id')) }),
|
|
85
90
|
};
|
|
91
|
+
if (resolved.deployment && network !== 'eip155:84532') {
|
|
92
|
+
throw new ArtifactBundleError('artifact_manifest_malformed', 'Pinned V2 deployment must use Base Sepolia');
|
|
93
|
+
}
|
|
86
94
|
const pinned = new Set(artifacts.map((artifact) => artifact.file));
|
|
87
95
|
for (const role of ARTIFACT_HASHES) {
|
|
88
96
|
const file = resolved.circuit[role];
|
|
@@ -92,6 +100,58 @@ export function parseCircuitManifest(value) {
|
|
|
92
100
|
}
|
|
93
101
|
return resolved;
|
|
94
102
|
}
|
|
103
|
+
function parseRelease(value) {
|
|
104
|
+
if (!isRecord(value)
|
|
105
|
+
|| typeof value.repository !== 'string'
|
|
106
|
+
|| !RELEASE_REPOSITORY_PATTERN.test(value.repository)
|
|
107
|
+
|| typeof value.tag !== 'string'
|
|
108
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.tag)
|
|
109
|
+
|| !Number.isSafeInteger(value.releaseId)
|
|
110
|
+
|| value.releaseId <= 0
|
|
111
|
+
|| typeof value.assetName !== 'string'
|
|
112
|
+
|| !SAFE_FILE_PATTERN.test(value.assetName)
|
|
113
|
+
|| value.assetName.includes('..')
|
|
114
|
+
|| typeof value.sha256 !== 'string'
|
|
115
|
+
|| !SHA256_PATTERN.test(value.sha256)) {
|
|
116
|
+
throw new ArtifactBundleError('artifact_manifest_malformed', 'Circuit manifest release pin is malformed');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
repository: value.repository,
|
|
120
|
+
tag: value.tag,
|
|
121
|
+
releaseId: value.releaseId,
|
|
122
|
+
assetName: value.assetName,
|
|
123
|
+
sha256: value.sha256,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseDeployment(value, circuitId) {
|
|
127
|
+
if (!isRecord(value)
|
|
128
|
+
|| value.chainId !== 84532
|
|
129
|
+
|| typeof value.bondAddress !== 'string'
|
|
130
|
+
|| !ADDRESS_PATTERN.test(value.bondAddress)
|
|
131
|
+
|| typeof value.deploymentBlock !== 'string'
|
|
132
|
+
|| !/^\d+$/u.test(value.deploymentBlock)
|
|
133
|
+
|| typeof value.deploymentDomain !== 'string'
|
|
134
|
+
|| value.deploymentDomain !== '84532'
|
|
135
|
+
|| value.circuitId !== circuitId
|
|
136
|
+
|| typeof value.verifyingKeyId !== 'string'
|
|
137
|
+
|| value.verifyingKeyId.length === 0
|
|
138
|
+
|| typeof value.spendVerifierAddress !== 'string'
|
|
139
|
+
|| !ADDRESS_PATTERN.test(value.spendVerifierAddress)
|
|
140
|
+
|| typeof value.groth16VerifierAddress !== 'string'
|
|
141
|
+
|| !ADDRESS_PATTERN.test(value.groth16VerifierAddress)) {
|
|
142
|
+
throw new ArtifactBundleError('artifact_manifest_malformed', 'Circuit manifest V2 deployment pin is malformed');
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
chainId: 84532,
|
|
146
|
+
bondAddress: value.bondAddress,
|
|
147
|
+
deploymentBlock: value.deploymentBlock,
|
|
148
|
+
deploymentDomain: value.deploymentDomain,
|
|
149
|
+
circuitId,
|
|
150
|
+
verifyingKeyId: value.verifyingKeyId,
|
|
151
|
+
spendVerifierAddress: value.spendVerifierAddress,
|
|
152
|
+
groth16VerifierAddress: value.groth16VerifierAddress,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
95
155
|
/** Loads the manifest shipped inside this package unless the caller supplies one. */
|
|
96
156
|
export async function loadCircuitManifest(manifestPath) {
|
|
97
157
|
const path = manifestPath ?? resolve(dirname(fileURLToPath(import.meta.url)), '..', 'circuits', 'manifest.json');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Authenticated, immutable-release acquisition for local proving material. */
|
|
2
|
+
import type { CircuitManifest } from './artifact-bundle.js';
|
|
3
|
+
export type GithubCommand = (arguments_: readonly string[]) => Promise<string>;
|
|
4
|
+
export interface AcquirePinnedBundleOptions {
|
|
5
|
+
manifest: CircuitManifest;
|
|
6
|
+
targetDirectory: string;
|
|
7
|
+
runGithubCommand?: GithubCommand;
|
|
8
|
+
}
|
|
9
|
+
/** Fetches a pinned private GitHub release and atomically installs its safe USTAR bundle. */
|
|
10
|
+
export declare function acquirePinnedArtifactBundle(options: AcquirePinnedBundleOptions): Promise<string>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/** Authenticated, immutable-release acquisition for local proving material. */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { execFile as nodeExecFile } from 'node:child_process';
|
|
4
|
+
import { gunzipSync } from 'node:zlib';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { chmod, lstat, mkdir, open, readFile, rename, rm, } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { resolvePinnedArtifactBundle } from './artifact-bundle.js';
|
|
9
|
+
const execFile = promisify(nodeExecFile);
|
|
10
|
+
const MAX_COMPRESSED_BYTES = 20 * 1024 * 1024;
|
|
11
|
+
const MAX_EXPANDED_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const BLOCK_BYTES = 512;
|
|
13
|
+
const SAFE_TAR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
14
|
+
function digest(bytes) {
|
|
15
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
function zeroBlock(block) {
|
|
18
|
+
return block.every((byte) => byte === 0);
|
|
19
|
+
}
|
|
20
|
+
function tarText(header, start, length, label) {
|
|
21
|
+
const bytes = header.subarray(start, start + length);
|
|
22
|
+
const terminator = bytes.indexOf(0);
|
|
23
|
+
const value = bytes.subarray(0, terminator < 0 ? bytes.length : terminator);
|
|
24
|
+
if (!value.every((byte) => byte >= 0x20 && byte <= 0x7e)) {
|
|
25
|
+
throw new Error('Pinned proving bundle archive contains an invalid ' + label);
|
|
26
|
+
}
|
|
27
|
+
return value.toString('ascii');
|
|
28
|
+
}
|
|
29
|
+
function tarOctal(header, start, length, label) {
|
|
30
|
+
const value = tarText(header, start, length, label).trim();
|
|
31
|
+
if (!value)
|
|
32
|
+
return 0;
|
|
33
|
+
if (!/^[0-7]+$/u.test(value))
|
|
34
|
+
throw new Error('Pinned proving bundle archive has an invalid ' + label);
|
|
35
|
+
const parsed = Number.parseInt(value, 8);
|
|
36
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
37
|
+
throw new Error('Pinned proving bundle archive has an invalid ' + label);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function parseTar(archive, allowedNames) {
|
|
41
|
+
if (archive.length === 0 || archive.length > MAX_EXPANDED_BYTES || archive.length % BLOCK_BYTES !== 0) {
|
|
42
|
+
throw new Error('Pinned proving bundle archive has an invalid size');
|
|
43
|
+
}
|
|
44
|
+
const files = new Map();
|
|
45
|
+
let offset = 0;
|
|
46
|
+
let endBlocks = 0;
|
|
47
|
+
while (offset + BLOCK_BYTES <= archive.length) {
|
|
48
|
+
const header = archive.subarray(offset, offset + BLOCK_BYTES);
|
|
49
|
+
if (zeroBlock(header)) {
|
|
50
|
+
endBlocks += 1;
|
|
51
|
+
offset += BLOCK_BYTES;
|
|
52
|
+
if (endBlocks === 2)
|
|
53
|
+
break;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (endBlocks > 0)
|
|
57
|
+
throw new Error('Pinned proving bundle archive has a malformed end marker');
|
|
58
|
+
const recordedChecksum = tarOctal(header, 148, 8, 'header checksum');
|
|
59
|
+
const checksumHeader = Buffer.from(header);
|
|
60
|
+
checksumHeader.fill(0x20, 148, 156);
|
|
61
|
+
const actualChecksum = checksumHeader.reduce((sum, byte) => sum + byte, 0);
|
|
62
|
+
if (recordedChecksum !== actualChecksum)
|
|
63
|
+
throw new Error('Pinned proving bundle archive failed its header checksum');
|
|
64
|
+
const magic = header.subarray(257, 263).toString('ascii');
|
|
65
|
+
if (magic !== 'ustar\0' && magic !== 'ustar ') {
|
|
66
|
+
throw new Error('Pinned proving bundle archive must use the USTAR format');
|
|
67
|
+
}
|
|
68
|
+
const name = tarText(header, 0, 100, 'entry name');
|
|
69
|
+
const prefix = tarText(header, 345, 155, 'entry prefix');
|
|
70
|
+
if (!name || prefix || !SAFE_TAR_NAME.test(name) || name.includes('..') || !allowedNames.has(name)) {
|
|
71
|
+
throw new Error('Pinned proving bundle archive contains an unexpected entry');
|
|
72
|
+
}
|
|
73
|
+
const type = header[156];
|
|
74
|
+
if (type !== 0 && type !== 0x30)
|
|
75
|
+
throw new Error('Pinned proving bundle archive contains a non-file entry');
|
|
76
|
+
if (tarText(header, 157, 100, 'link target'))
|
|
77
|
+
throw new Error('Pinned proving bundle archive contains a link');
|
|
78
|
+
if (files.has(name))
|
|
79
|
+
throw new Error('Pinned proving bundle archive contains a duplicate entry');
|
|
80
|
+
const size = tarOctal(header, 124, 12, 'entry size');
|
|
81
|
+
const dataStart = offset + BLOCK_BYTES;
|
|
82
|
+
const dataEnd = dataStart + size;
|
|
83
|
+
const paddedEnd = dataStart + Math.ceil(size / BLOCK_BYTES) * BLOCK_BYTES;
|
|
84
|
+
if (size <= 0 || dataEnd > archive.length || paddedEnd > archive.length) {
|
|
85
|
+
throw new Error('Pinned proving bundle archive entry exceeds its bounds');
|
|
86
|
+
}
|
|
87
|
+
if (!zeroBlock(archive.subarray(dataEnd, paddedEnd))) {
|
|
88
|
+
throw new Error('Pinned proving bundle archive has invalid entry padding');
|
|
89
|
+
}
|
|
90
|
+
files.set(name, Buffer.from(archive.subarray(dataStart, dataEnd)));
|
|
91
|
+
offset = paddedEnd;
|
|
92
|
+
}
|
|
93
|
+
if (endBlocks !== 2)
|
|
94
|
+
throw new Error('Pinned proving bundle archive is missing its end marker');
|
|
95
|
+
if (!zeroBlock(archive.subarray(offset)))
|
|
96
|
+
throw new Error('Pinned proving bundle archive has trailing data');
|
|
97
|
+
if (files.size !== allowedNames.size || [...allowedNames].some((name) => !files.has(name))) {
|
|
98
|
+
throw new Error('Pinned proving bundle archive is incomplete');
|
|
99
|
+
}
|
|
100
|
+
return files;
|
|
101
|
+
}
|
|
102
|
+
async function defaultGithubCommand(arguments_) {
|
|
103
|
+
try {
|
|
104
|
+
const { stdout } = await execFile('gh', [...arguments_], {
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
timeout: 60_000,
|
|
107
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
});
|
|
110
|
+
return stdout;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error('Pinned proving release could not be read; authenticate with gh and confirm repository access.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function pinnedAsset(release, raw) {
|
|
117
|
+
let value;
|
|
118
|
+
try {
|
|
119
|
+
value = JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error('Pinned proving release metadata is invalid');
|
|
123
|
+
}
|
|
124
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
125
|
+
throw new Error('Pinned proving release metadata is invalid');
|
|
126
|
+
}
|
|
127
|
+
const record = value;
|
|
128
|
+
const assets = Array.isArray(record.assets) ? record.assets : [];
|
|
129
|
+
const asset = assets.find((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
130
|
+
&& entry.name === release.assetName);
|
|
131
|
+
if (record.id !== release.releaseId
|
|
132
|
+
|| record.tag_name !== release.tag
|
|
133
|
+
|| record.draft !== false
|
|
134
|
+
|| record.immutable !== true
|
|
135
|
+
|| !asset
|
|
136
|
+
|| (typeof asset.digest === 'string' && asset.digest !== 'sha256:' + release.sha256)) {
|
|
137
|
+
throw new Error('Pinned proving release identity or immutability check failed');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function writeBundleFile(directory, name, bytes) {
|
|
141
|
+
const file = await open(join(directory, name), 'wx', 0o600);
|
|
142
|
+
try {
|
|
143
|
+
await file.writeFile(bytes);
|
|
144
|
+
await file.sync();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await file.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function replaceDirectoryAtomically(stage, target) {
|
|
151
|
+
const parent = dirname(target);
|
|
152
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
153
|
+
const backup = join(parent, '.base-sepolia-old-' + randomUUID());
|
|
154
|
+
let movedOld = false;
|
|
155
|
+
try {
|
|
156
|
+
try {
|
|
157
|
+
const existing = await lstat(target);
|
|
158
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
159
|
+
throw new Error('Local proving bundle destination must be a regular directory');
|
|
160
|
+
}
|
|
161
|
+
await rename(target, backup);
|
|
162
|
+
movedOld = true;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
166
|
+
// No previous bundle is a normal first-run case.
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await rename(stage, target);
|
|
174
|
+
await chmod(target, 0o700);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (movedOld)
|
|
178
|
+
await rename(backup, target);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (movedOld)
|
|
182
|
+
await rm(backup, { recursive: true, force: true }).catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await rm(stage, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Fetches a pinned private GitHub release and atomically installs its safe USTAR bundle. */
|
|
189
|
+
export async function acquirePinnedArtifactBundle(options) {
|
|
190
|
+
const release = options.manifest.release;
|
|
191
|
+
if (!release)
|
|
192
|
+
throw new Error('This sidecar manifest has no pinned artifact release');
|
|
193
|
+
const names = new Set(options.manifest.artifacts.map((artifact) => artifact.file));
|
|
194
|
+
const command = options.runGithubCommand ?? defaultGithubCommand;
|
|
195
|
+
const parent = dirname(resolve(options.targetDirectory));
|
|
196
|
+
const tempDirectory = join(parent, '.base-sepolia-download-' + randomUUID());
|
|
197
|
+
const stage = join(parent, '.base-sepolia-stage-' + randomUUID());
|
|
198
|
+
try {
|
|
199
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
200
|
+
await mkdir(tempDirectory, { recursive: false, mode: 0o700 });
|
|
201
|
+
await mkdir(stage, { recursive: false, mode: 0o700 });
|
|
202
|
+
const releaseJson = await command(['api', `repos/${release.repository}/releases/${release.releaseId}`]);
|
|
203
|
+
pinnedAsset(release, releaseJson);
|
|
204
|
+
await command([
|
|
205
|
+
'release', 'download', release.tag,
|
|
206
|
+
'--repo', release.repository,
|
|
207
|
+
'--dir', tempDirectory,
|
|
208
|
+
'--pattern', release.assetName,
|
|
209
|
+
]);
|
|
210
|
+
const archivePath = join(tempDirectory, release.assetName);
|
|
211
|
+
const archiveInfo = await lstat(archivePath);
|
|
212
|
+
if (archiveInfo.isSymbolicLink() || !archiveInfo.isFile() || archiveInfo.size > MAX_COMPRESSED_BYTES) {
|
|
213
|
+
throw new Error('Pinned proving bundle archive is missing or too large');
|
|
214
|
+
}
|
|
215
|
+
const compressed = await readFile(archivePath);
|
|
216
|
+
if (digest(compressed) !== release.sha256)
|
|
217
|
+
throw new Error('Pinned proving bundle archive failed its SHA-256 check');
|
|
218
|
+
let expanded;
|
|
219
|
+
try {
|
|
220
|
+
expanded = gunzipSync(compressed, { maxOutputLength: MAX_EXPANDED_BYTES });
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new Error('Pinned proving bundle archive is not a bounded gzip tarball');
|
|
224
|
+
}
|
|
225
|
+
const files = parseTar(expanded, names);
|
|
226
|
+
for (const [name, bytes] of files)
|
|
227
|
+
await writeBundleFile(stage, name, bytes);
|
|
228
|
+
await chmod(stage, 0o700);
|
|
229
|
+
await resolvePinnedArtifactBundle({ artifactDirectory: stage, manifest: options.manifest });
|
|
230
|
+
await replaceDirectoryAtomically(stage, resolve(options.targetDirectory));
|
|
231
|
+
await resolvePinnedArtifactBundle({ artifactDirectory: resolve(options.targetDirectory), manifest: options.manifest });
|
|
232
|
+
return resolve(options.targetDirectory);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof Error && /^(Pinned proving|This sidecar manifest|Local proving bundle)/u.test(error.message))
|
|
236
|
+
throw error;
|
|
237
|
+
throw new Error('Pinned proving bundle acquisition failed; verify gh access and the published release pin.');
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
241
|
+
await rm(stage, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|