gitchain-sol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/gitchain.mjs +853 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# gitchain
|
|
2
|
+
|
|
3
|
+
Software provenance & lineage protocol on Solana. Fingerprint your code, register it on-chain, and verify the origin of any repository.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g gitchain
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires a [Solana wallet](https://docs.solana.com/cli/install-solana-cli-tools):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
solana-keygen new # generate wallet (one-time)
|
|
15
|
+
solana airdrop 2 --url devnet # fund it on devnet
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Initialize a project
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd your-project
|
|
24
|
+
gitchain init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Publish to Solana + push
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gitchain publish origin main
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This will:
|
|
34
|
+
1. Fingerprint all source files (SHA-256 Merkle tree)
|
|
35
|
+
2. Register/update the fingerprint on Solana devnet
|
|
36
|
+
3. `git push` to your remote
|
|
37
|
+
|
|
38
|
+
### Clone with provenance verification
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
gitchain clone https://github.com/user/repo.git
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This will:
|
|
45
|
+
1. Query Solana for any on-chain provenance record
|
|
46
|
+
2. `git clone` the repository
|
|
47
|
+
3. Verify the cloned code matches the on-chain fingerprint
|
|
48
|
+
4. Initialize `.gitchain/` tracking in the cloned repo
|
|
49
|
+
|
|
50
|
+
### Verify code against on-chain records
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
gitchain verify ./suspect-code
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Compares a local directory against all registered projects on Solana and reports lineage matches.
|
|
57
|
+
|
|
58
|
+
### Check status
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
gitchain status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Shows local and on-chain provenance state for the current project.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
GitChain creates a multi-layer fingerprint of your source code:
|
|
69
|
+
|
|
70
|
+
1. **File hashes** — SHA-256 hash of each source file (with CRLF/LF normalization)
|
|
71
|
+
2. **Merkle root** — Merkle tree root of all file hashes
|
|
72
|
+
3. **File hashes hash** — SHA-256 of the sorted file hash map (secondary verification)
|
|
73
|
+
|
|
74
|
+
These are stored in a Solana Program Derived Address (PDA) tied to your wallet and project name. Anyone cloning or verifying code can check it against the on-chain record.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
// src/utils/config.ts
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { join, basename } from "node:path";
|
|
13
|
+
var GITCHAIN_DIR = ".gitchain";
|
|
14
|
+
var CONFIG_FILE = "config.json";
|
|
15
|
+
var PROVENANCE_FILE = "provenance.json";
|
|
16
|
+
function getGitChainDir(projectPath) {
|
|
17
|
+
return join(projectPath, GITCHAIN_DIR);
|
|
18
|
+
}
|
|
19
|
+
function isGitRepo(projectPath) {
|
|
20
|
+
return existsSync(join(projectPath, ".git"));
|
|
21
|
+
}
|
|
22
|
+
function isInitialized(projectPath) {
|
|
23
|
+
return existsSync(join(getGitChainDir(projectPath), CONFIG_FILE));
|
|
24
|
+
}
|
|
25
|
+
function readConfig(projectPath) {
|
|
26
|
+
const configPath = join(getGitChainDir(projectPath), CONFIG_FILE);
|
|
27
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
function writeConfig(projectPath, config) {
|
|
30
|
+
const dir = getGitChainDir(projectPath);
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
writeFileSync(join(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
33
|
+
}
|
|
34
|
+
function readProvenance(projectPath) {
|
|
35
|
+
const provPath = join(getGitChainDir(projectPath), PROVENANCE_FILE);
|
|
36
|
+
if (!existsSync(provPath)) return { published: false };
|
|
37
|
+
return JSON.parse(readFileSync(provPath, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
function writeProvenance(projectPath, state) {
|
|
40
|
+
const provPath = join(getGitChainDir(projectPath), PROVENANCE_FILE);
|
|
41
|
+
writeFileSync(provPath, JSON.stringify(state, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
function getProjectName(projectPath) {
|
|
44
|
+
try {
|
|
45
|
+
const gitConfigPath = join(projectPath, ".git", "config");
|
|
46
|
+
const gitConfig = readFileSync(gitConfigPath, "utf-8");
|
|
47
|
+
const match = gitConfig.match(/url\s*=\s*.*[/:]([^/]+?)(?:\.git)?\s*$/m);
|
|
48
|
+
if (match) return match[1];
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
return basename(projectPath);
|
|
52
|
+
}
|
|
53
|
+
function ensureGitignore(projectPath) {
|
|
54
|
+
const gitignorePath = join(projectPath, ".gitignore");
|
|
55
|
+
if (existsSync(gitignorePath)) {
|
|
56
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
57
|
+
if (!content.includes(".gitchain/")) {
|
|
58
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n.gitchain/\n");
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
writeFileSync(gitignorePath, ".gitchain/\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/commands/init.ts
|
|
66
|
+
var DEFAULT_WALLET_PATH = process.platform === "win32" ? `${process.env.USERPROFILE}\\.config\\solana\\id.json` : `${process.env.HOME}/.config/solana/id.json`;
|
|
67
|
+
var DEFAULT_RPC_URL = "https://api.devnet.solana.com";
|
|
68
|
+
var DEFAULT_PROGRAM_ID = "5bHSYFLoEtoxQnqi6vuDvjFGbcuwnLJbi6wJbHmHW8R4";
|
|
69
|
+
async function initCommand(options) {
|
|
70
|
+
const projectPath = resolve(options.path || ".");
|
|
71
|
+
if (!isGitRepo(projectPath)) {
|
|
72
|
+
console.error(chalk.red("Error: Not a Git repository."));
|
|
73
|
+
console.error(chalk.gray("Run 'git init' first, or navigate to a Git repo."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (isInitialized(projectPath)) {
|
|
77
|
+
console.log(chalk.yellow("Already initialized."));
|
|
78
|
+
console.log(chalk.gray("Run 'gitchain status' to see current state."));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const projectName = getProjectName(projectPath);
|
|
82
|
+
const config = {
|
|
83
|
+
projectName,
|
|
84
|
+
walletPath: DEFAULT_WALLET_PATH,
|
|
85
|
+
rpcUrl: DEFAULT_RPC_URL,
|
|
86
|
+
programId: DEFAULT_PROGRAM_ID
|
|
87
|
+
};
|
|
88
|
+
writeConfig(projectPath, config);
|
|
89
|
+
writeProvenance(projectPath, { published: false });
|
|
90
|
+
ensureGitignore(projectPath);
|
|
91
|
+
console.log(chalk.green(`
|
|
92
|
+
GitChain initialized for "${projectName}"
|
|
93
|
+
`));
|
|
94
|
+
console.log(chalk.gray(" Config: .gitchain/config.json"));
|
|
95
|
+
console.log(chalk.gray(" Provenance: .gitchain/provenance.json"));
|
|
96
|
+
console.log(chalk.gray(` Wallet: ${config.walletPath}`));
|
|
97
|
+
console.log(chalk.gray(` RPC: ${config.rpcUrl}`));
|
|
98
|
+
console.log(chalk.gray("\nNext: run 'gitchain publish' to register on Solana."));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/commands/publish.ts
|
|
102
|
+
import chalk2 from "chalk";
|
|
103
|
+
import { resolve as resolve2 } from "node:path";
|
|
104
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
105
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
106
|
+
import { execSync } from "node:child_process";
|
|
107
|
+
import { SystemProgram } from "@solana/web3.js";
|
|
108
|
+
|
|
109
|
+
// ../fingerprint/dist/hash.js
|
|
110
|
+
import { createHash } from "node:crypto";
|
|
111
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
112
|
+
import { join as join2, relative } from "node:path";
|
|
113
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
114
|
+
".git",
|
|
115
|
+
"node_modules",
|
|
116
|
+
".gitchain",
|
|
117
|
+
"target",
|
|
118
|
+
".anchor",
|
|
119
|
+
"dist",
|
|
120
|
+
"test-ledger",
|
|
121
|
+
".next"
|
|
122
|
+
]);
|
|
123
|
+
var EXCLUDED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
124
|
+
".png",
|
|
125
|
+
".jpg",
|
|
126
|
+
".jpeg",
|
|
127
|
+
".gif",
|
|
128
|
+
".ico",
|
|
129
|
+
".woff",
|
|
130
|
+
".woff2",
|
|
131
|
+
".ttf",
|
|
132
|
+
".eot",
|
|
133
|
+
".mp3",
|
|
134
|
+
".mp4",
|
|
135
|
+
".mov",
|
|
136
|
+
".zip",
|
|
137
|
+
".tar",
|
|
138
|
+
".gz"
|
|
139
|
+
]);
|
|
140
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
141
|
+
...EXCLUDED_EXTENSIONS,
|
|
142
|
+
".wasm",
|
|
143
|
+
".so",
|
|
144
|
+
".dll",
|
|
145
|
+
".exe",
|
|
146
|
+
".bin",
|
|
147
|
+
".dat"
|
|
148
|
+
]);
|
|
149
|
+
async function hashFile(filePath) {
|
|
150
|
+
const content = await readFile(filePath);
|
|
151
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
152
|
+
if (!BINARY_EXTENSIONS.has(ext)) {
|
|
153
|
+
let normalized = content.toString("utf-8").replace(/\r\n/g, "\n");
|
|
154
|
+
if (filePath.endsWith(".gitignore")) {
|
|
155
|
+
normalized = normalized.split("\n").filter((line) => line.trim() !== ".gitchain/").join("\n").trimEnd() + "\n";
|
|
156
|
+
}
|
|
157
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
158
|
+
}
|
|
159
|
+
return createHash("sha256").update(content).digest("hex");
|
|
160
|
+
}
|
|
161
|
+
async function hashDirectory(dirPath) {
|
|
162
|
+
const fileHashes = {};
|
|
163
|
+
await walkDir(dirPath, dirPath, fileHashes);
|
|
164
|
+
const contentHashes = Object.values(fileHashes).sort();
|
|
165
|
+
return {
|
|
166
|
+
fileHashes,
|
|
167
|
+
contentHashes,
|
|
168
|
+
fileCount: Object.keys(fileHashes).length
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function walkDir(basePath, currentPath, result) {
|
|
172
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
const fullPath = join2(currentPath, entry.name);
|
|
175
|
+
if (entry.isDirectory()) {
|
|
176
|
+
if (!EXCLUDED_DIRS.has(entry.name)) {
|
|
177
|
+
await walkDir(basePath, fullPath, result);
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (entry.isFile()) {
|
|
182
|
+
const ext = entry.name.substring(entry.name.lastIndexOf(".")).toLowerCase();
|
|
183
|
+
if (EXCLUDED_EXTENSIONS.has(ext))
|
|
184
|
+
continue;
|
|
185
|
+
const relativePath = relative(basePath, fullPath).split("\\").join("/");
|
|
186
|
+
const hash = await hashFile(fullPath);
|
|
187
|
+
result[relativePath] = hash;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ../fingerprint/dist/merkle.js
|
|
193
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
194
|
+
function sha256(data) {
|
|
195
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
196
|
+
}
|
|
197
|
+
function buildMerkleTree(leaves) {
|
|
198
|
+
if (leaves.length === 0) {
|
|
199
|
+
return { root: sha256(""), leaves: [], depth: 0 };
|
|
200
|
+
}
|
|
201
|
+
const sortedLeaves = [...leaves].sort();
|
|
202
|
+
let currentLevel = sortedLeaves.map((l) => sha256(l));
|
|
203
|
+
let depth = 0;
|
|
204
|
+
while (currentLevel.length > 1) {
|
|
205
|
+
const nextLevel = [];
|
|
206
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
207
|
+
const left = currentLevel[i];
|
|
208
|
+
const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : currentLevel[i];
|
|
209
|
+
nextLevel.push(sha256(left + right));
|
|
210
|
+
}
|
|
211
|
+
currentLevel = nextLevel;
|
|
212
|
+
depth++;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
root: currentLevel[0],
|
|
216
|
+
leaves: sortedLeaves,
|
|
217
|
+
depth
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function computeMerkleRoot(contentHashes) {
|
|
221
|
+
return buildMerkleTree(contentHashes).root;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ../fingerprint/dist/compare.js
|
|
225
|
+
function classifyOverlap(overlapPercent) {
|
|
226
|
+
if (overlapPercent >= 90)
|
|
227
|
+
return "Disputed Origin";
|
|
228
|
+
if (overlapPercent >= 60)
|
|
229
|
+
return "High Derivation Likelihood";
|
|
230
|
+
if (overlapPercent >= 30)
|
|
231
|
+
return "Possible Derivation";
|
|
232
|
+
return "No significant similarity";
|
|
233
|
+
}
|
|
234
|
+
function compareFingerprints(source, suspect) {
|
|
235
|
+
const merkleMatch = source.merkleRoot === suspect.merkleRoot;
|
|
236
|
+
const sourceContentSet = new Set(source.contentHashes);
|
|
237
|
+
const suspectContentSet = new Set(suspect.contentHashes);
|
|
238
|
+
let matchedFiles = 0;
|
|
239
|
+
for (const hash of suspectContentSet) {
|
|
240
|
+
if (sourceContentSet.has(hash)) {
|
|
241
|
+
matchedFiles++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const denominator = Math.min(source.fileCount, suspect.fileCount);
|
|
245
|
+
const fileOverlapPercent = denominator === 0 ? 0 : matchedFiles / denominator * 100;
|
|
246
|
+
const confidence = merkleMatch ? 100 : Math.min(fileOverlapPercent * 1.05, 99);
|
|
247
|
+
const status = classifyOverlap(fileOverlapPercent);
|
|
248
|
+
return {
|
|
249
|
+
merkleMatch,
|
|
250
|
+
fileOverlapPercent,
|
|
251
|
+
matchedFiles,
|
|
252
|
+
totalFilesCompared: denominator,
|
|
253
|
+
confidence: Math.round(confidence),
|
|
254
|
+
status
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ../fingerprint/dist/index.js
|
|
259
|
+
async function fingerprintDirectory(dirPath) {
|
|
260
|
+
const dirResult = await hashDirectory(dirPath);
|
|
261
|
+
const merkleRoot = computeMerkleRoot(dirResult.contentHashes);
|
|
262
|
+
return {
|
|
263
|
+
merkleRoot,
|
|
264
|
+
fileHashes: dirResult.fileHashes,
|
|
265
|
+
contentHashes: dirResult.contentHashes,
|
|
266
|
+
fileCount: dirResult.fileCount
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/utils/solana.ts
|
|
271
|
+
import {
|
|
272
|
+
Connection,
|
|
273
|
+
PublicKey,
|
|
274
|
+
Keypair
|
|
275
|
+
} from "@solana/web3.js";
|
|
276
|
+
import { AnchorProvider, Program, Wallet, setProvider } from "@coral-xyz/anchor";
|
|
277
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
278
|
+
var PROGRAM_ID_STRING = "5bHSYFLoEtoxQnqi6vuDvjFGbcuwnLJbi6wJbHmHW8R4";
|
|
279
|
+
var IDL = {
|
|
280
|
+
address: PROGRAM_ID_STRING,
|
|
281
|
+
metadata: { name: "gitchain", version: "0.1.0", spec: "0.1.0" },
|
|
282
|
+
instructions: [
|
|
283
|
+
{
|
|
284
|
+
name: "register_project",
|
|
285
|
+
discriminator: [130, 150, 121, 216, 183, 225, 243, 192],
|
|
286
|
+
accounts: [
|
|
287
|
+
{ name: "project_record", writable: true },
|
|
288
|
+
{ name: "creator", writable: true, signer: true },
|
|
289
|
+
{ name: "system_program", address: "11111111111111111111111111111111" }
|
|
290
|
+
],
|
|
291
|
+
args: [
|
|
292
|
+
{ name: "name", type: "string" },
|
|
293
|
+
{ name: "merkle_root", type: { array: ["u8", 32] } },
|
|
294
|
+
{ name: "file_hashes_hash", type: { array: ["u8", 32] } },
|
|
295
|
+
{ name: "file_count", type: "u16" },
|
|
296
|
+
{ name: "repo_url", type: "string" }
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "update_fingerprint",
|
|
301
|
+
discriminator: [160, 228, 44, 176, 237, 68, 236, 123],
|
|
302
|
+
accounts: [
|
|
303
|
+
{ name: "project_record", writable: true },
|
|
304
|
+
{ name: "creator", signer: true }
|
|
305
|
+
],
|
|
306
|
+
args: [
|
|
307
|
+
{ name: "merkle_root", type: { array: ["u8", 32] } },
|
|
308
|
+
{ name: "file_hashes_hash", type: { array: ["u8", 32] } },
|
|
309
|
+
{ name: "file_count", type: "u16" }
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "update_status",
|
|
314
|
+
discriminator: [147, 215, 74, 174, 55, 191, 42, 0],
|
|
315
|
+
accounts: [
|
|
316
|
+
{ name: "project_record", writable: true },
|
|
317
|
+
{ name: "creator", signer: true }
|
|
318
|
+
],
|
|
319
|
+
args: [
|
|
320
|
+
{ name: "new_status", type: "u8" },
|
|
321
|
+
{ name: "parent", type: "pubkey" },
|
|
322
|
+
{ name: "origin", type: "pubkey" }
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
],
|
|
326
|
+
accounts: [
|
|
327
|
+
{
|
|
328
|
+
name: "ProjectRecord",
|
|
329
|
+
discriminator: [93, 174, 112, 203, 231, 123, 92, 56]
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
types: [
|
|
333
|
+
{
|
|
334
|
+
name: "ProjectRecord",
|
|
335
|
+
type: {
|
|
336
|
+
kind: "struct",
|
|
337
|
+
fields: [
|
|
338
|
+
{ name: "creator", type: "pubkey" },
|
|
339
|
+
{ name: "project_name", type: "string" },
|
|
340
|
+
{ name: "merkle_root", type: { array: ["u8", 32] } },
|
|
341
|
+
{ name: "file_hashes_hash", type: { array: ["u8", 32] } },
|
|
342
|
+
{ name: "file_count", type: "u16" },
|
|
343
|
+
{ name: "status", type: "u8" },
|
|
344
|
+
{ name: "parent_project", type: "pubkey" },
|
|
345
|
+
{ name: "origin_project", type: "pubkey" },
|
|
346
|
+
{ name: "registered_at", type: "i64" },
|
|
347
|
+
{ name: "repo_url", type: "string" },
|
|
348
|
+
{ name: "bump", type: "u8" }
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
function loadKeypair(walletPath) {
|
|
355
|
+
const raw = readFileSync2(walletPath, "utf-8");
|
|
356
|
+
const secretKey = Uint8Array.from(JSON.parse(raw));
|
|
357
|
+
return Keypair.fromSecretKey(secretKey);
|
|
358
|
+
}
|
|
359
|
+
function getConnection(rpcUrl) {
|
|
360
|
+
return new Connection(rpcUrl, "confirmed");
|
|
361
|
+
}
|
|
362
|
+
function getProgramId() {
|
|
363
|
+
return new PublicKey(PROGRAM_ID_STRING);
|
|
364
|
+
}
|
|
365
|
+
function deriveProjectPda(creatorPubkey, projectName) {
|
|
366
|
+
return PublicKey.findProgramAddressSync(
|
|
367
|
+
[
|
|
368
|
+
Buffer.from("project"),
|
|
369
|
+
creatorPubkey.toBuffer(),
|
|
370
|
+
Buffer.from(projectName)
|
|
371
|
+
],
|
|
372
|
+
getProgramId()
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function getProgram(connection, keypair) {
|
|
376
|
+
const wallet = new Wallet(keypair);
|
|
377
|
+
const provider = new AnchorProvider(connection, wallet, {
|
|
378
|
+
commitment: "confirmed"
|
|
379
|
+
});
|
|
380
|
+
setProvider(provider);
|
|
381
|
+
return new Program(IDL, provider);
|
|
382
|
+
}
|
|
383
|
+
function hexToBytes32(hex) {
|
|
384
|
+
const buf = Buffer.from(hex, "hex");
|
|
385
|
+
return Array.from(buf.subarray(0, 32));
|
|
386
|
+
}
|
|
387
|
+
async function fetchAllProjectsViaProgram(program2) {
|
|
388
|
+
return await program2.account.projectRecord.all();
|
|
389
|
+
}
|
|
390
|
+
var STATUS_LABELS = {
|
|
391
|
+
0: "Registered",
|
|
392
|
+
1: "Verified Original",
|
|
393
|
+
2: "Verified Derivative",
|
|
394
|
+
3: "Disputed Origin",
|
|
395
|
+
4: "Provenance Stripped"
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/commands/publish.ts
|
|
399
|
+
async function publishCommand(remote, branch, options) {
|
|
400
|
+
const projectPath = resolve2(options.path || ".");
|
|
401
|
+
if (!isInitialized(projectPath)) {
|
|
402
|
+
console.error(chalk2.red("Error: Not initialized. Run 'gitchain init' first."));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const config = readConfig(projectPath);
|
|
406
|
+
if (!existsSync2(config.walletPath)) {
|
|
407
|
+
console.error(chalk2.red(`Error: Wallet not found at ${config.walletPath}`));
|
|
408
|
+
console.error(chalk2.gray("Generate one with: solana-keygen new"));
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
console.log(chalk2.gray("Fingerprinting project..."));
|
|
412
|
+
const fingerprint = await fingerprintDirectory(projectPath);
|
|
413
|
+
console.log(chalk2.gray(` Files scanned: ${fingerprint.fileCount}`));
|
|
414
|
+
console.log(chalk2.gray(` Merkle root: ${fingerprint.merkleRoot.substring(0, 16)}...`));
|
|
415
|
+
const provenance = readProvenance(projectPath);
|
|
416
|
+
const codeChanged = provenance.merkleRoot !== fingerprint.merkleRoot;
|
|
417
|
+
if (!codeChanged && provenance.published) {
|
|
418
|
+
console.log(chalk2.yellow("\nNo code changes since last publish. Skipping Solana registration."));
|
|
419
|
+
console.log(chalk2.gray(`Pushing to ${remote}/${branch}...
|
|
420
|
+
`));
|
|
421
|
+
try {
|
|
422
|
+
execSync(`git push ${remote} ${branch}`, {
|
|
423
|
+
cwd: projectPath,
|
|
424
|
+
stdio: "inherit"
|
|
425
|
+
});
|
|
426
|
+
console.log(chalk2.green(`
|
|
427
|
+
Pushed to ${remote}/${branch}.`));
|
|
428
|
+
} catch {
|
|
429
|
+
console.error(chalk2.red(`
|
|
430
|
+
git push failed. Resolve the issue and retry.`));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const fileHashesJson = JSON.stringify(
|
|
436
|
+
Object.fromEntries(
|
|
437
|
+
Object.entries(fingerprint.fileHashes).sort(([a], [b]) => a.localeCompare(b))
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
const fileHashesHash = createHash3("sha256").update(fileHashesJson).digest("hex");
|
|
441
|
+
console.log(chalk2.gray("\nConnecting to Solana..."));
|
|
442
|
+
const keypair = loadKeypair(config.walletPath);
|
|
443
|
+
const connection = getConnection(config.rpcUrl);
|
|
444
|
+
const program2 = getProgram(connection, keypair);
|
|
445
|
+
const [pda] = deriveProjectPda(keypair.publicKey, config.projectName);
|
|
446
|
+
console.log(chalk2.gray(` Wallet: ${keypair.publicKey.toString()}`));
|
|
447
|
+
console.log(chalk2.gray(` PDA: ${pda.toString()}`));
|
|
448
|
+
let alreadyRegistered = false;
|
|
449
|
+
try {
|
|
450
|
+
const existing = await program2.account.projectRecord.fetch(pda);
|
|
451
|
+
if (existing) alreadyRegistered = true;
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
let txSignature = provenance.txSignature || "";
|
|
455
|
+
if (!alreadyRegistered) {
|
|
456
|
+
console.log(chalk2.gray("\nRegistering on Solana..."));
|
|
457
|
+
try {
|
|
458
|
+
txSignature = await program2.methods.registerProject(
|
|
459
|
+
config.projectName,
|
|
460
|
+
hexToBytes32(fingerprint.merkleRoot),
|
|
461
|
+
hexToBytes32(fileHashesHash),
|
|
462
|
+
fingerprint.fileCount,
|
|
463
|
+
getRepoUrl(projectPath)
|
|
464
|
+
).accounts({
|
|
465
|
+
projectRecord: pda,
|
|
466
|
+
creator: keypair.publicKey,
|
|
467
|
+
systemProgram: SystemProgram.programId
|
|
468
|
+
}).rpc();
|
|
469
|
+
console.log(chalk2.green(" Solana registration successful."));
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error(chalk2.red(`
|
|
472
|
+
Solana transaction failed: ${err.message}`));
|
|
473
|
+
console.error(chalk2.red("Aborting push. Fix the issue and retry."));
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
console.log(chalk2.gray("\nUpdating fingerprint on Solana..."));
|
|
478
|
+
try {
|
|
479
|
+
txSignature = await program2.methods.updateFingerprint(
|
|
480
|
+
hexToBytes32(fingerprint.merkleRoot),
|
|
481
|
+
hexToBytes32(fileHashesHash),
|
|
482
|
+
fingerprint.fileCount
|
|
483
|
+
).accounts({
|
|
484
|
+
projectRecord: pda,
|
|
485
|
+
creator: keypair.publicKey
|
|
486
|
+
}).rpc();
|
|
487
|
+
console.log(chalk2.green(" Fingerprint updated on Solana."));
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.error(chalk2.red(`
|
|
490
|
+
Solana update failed: ${err.message}`));
|
|
491
|
+
console.error(chalk2.red("Aborting push. Fix the issue and retry."));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
console.log(chalk2.gray(`
|
|
496
|
+
Pushing to ${remote}/${branch}...`));
|
|
497
|
+
try {
|
|
498
|
+
execSync(`git push ${remote} ${branch}`, {
|
|
499
|
+
cwd: projectPath,
|
|
500
|
+
stdio: "inherit"
|
|
501
|
+
});
|
|
502
|
+
} catch {
|
|
503
|
+
console.error(chalk2.red(`
|
|
504
|
+
git push failed. Your code IS registered on Solana.`));
|
|
505
|
+
console.error(chalk2.gray("Resolve the git issue and run: git push " + remote + " " + branch));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
writeProvenance(projectPath, {
|
|
509
|
+
published: true,
|
|
510
|
+
merkleRoot: fingerprint.merkleRoot,
|
|
511
|
+
fileHashesHash,
|
|
512
|
+
fileCount: fingerprint.fileCount,
|
|
513
|
+
txSignature,
|
|
514
|
+
pdaAddress: pda.toString(),
|
|
515
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
516
|
+
});
|
|
517
|
+
console.log(chalk2.green("\nPublished & pushed!\n"));
|
|
518
|
+
console.log(` ${chalk2.bold("Merkle Root:")} ${fingerprint.merkleRoot}`);
|
|
519
|
+
console.log(` ${chalk2.bold("Files:")} ${fingerprint.fileCount}`);
|
|
520
|
+
console.log(` ${chalk2.bold("Solana TX:")} ${txSignature}`);
|
|
521
|
+
console.log(` ${chalk2.bold("Pushed to:")} ${remote}/${branch}`);
|
|
522
|
+
console.log(
|
|
523
|
+
chalk2.gray(`
|
|
524
|
+
Explorer: https://explorer.solana.com/tx/${txSignature}?cluster=devnet`)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
function getRepoUrl(projectPath) {
|
|
528
|
+
try {
|
|
529
|
+
return execSync("git remote get-url origin", { cwd: projectPath, encoding: "utf-8" }).trim();
|
|
530
|
+
} catch {
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/commands/clone.ts
|
|
536
|
+
import chalk3 from "chalk";
|
|
537
|
+
import { resolve as resolve3, basename as basename2 } from "node:path";
|
|
538
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
539
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
540
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
541
|
+
var DEFAULT_WALLET_PATH2 = process.platform === "win32" ? `${process.env.USERPROFILE}\\.config\\solana\\id.json` : `${process.env.HOME}/.config/solana/id.json`;
|
|
542
|
+
var DEFAULT_RPC_URL2 = "https://api.devnet.solana.com";
|
|
543
|
+
var DEFAULT_PROGRAM_ID2 = "5bHSYFLoEtoxQnqi6vuDvjFGbcuwnLJbi6wJbHmHW8R4";
|
|
544
|
+
async function cloneCommand(url, options) {
|
|
545
|
+
const repoName = extractRepoName(url);
|
|
546
|
+
const targetDir = resolve3(options.directory || repoName);
|
|
547
|
+
console.log(chalk3.bold(`
|
|
548
|
+
GitChain Clone: ${repoName}
|
|
549
|
+
`));
|
|
550
|
+
console.log(chalk3.gray("Checking on-chain provenance..."));
|
|
551
|
+
let onChainRecord = null;
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync3(DEFAULT_WALLET_PATH2)) {
|
|
554
|
+
const keypair = loadKeypair(DEFAULT_WALLET_PATH2);
|
|
555
|
+
const connection = getConnection(DEFAULT_RPC_URL2);
|
|
556
|
+
const program2 = getProgram(connection, keypair);
|
|
557
|
+
const allProjects = await fetchAllProjectsViaProgram(program2);
|
|
558
|
+
for (const item of allProjects) {
|
|
559
|
+
const account = item.account;
|
|
560
|
+
const accountRepoUrl = (account.repoUrl || "").replace(/\.git$/, "").toLowerCase();
|
|
561
|
+
const searchUrl = url.replace(/\.git$/, "").toLowerCase();
|
|
562
|
+
if (accountRepoUrl && (accountRepoUrl === searchUrl || accountRepoUrl.includes(repoName.toLowerCase()))) {
|
|
563
|
+
const registeredAt = account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt);
|
|
564
|
+
onChainRecord = {
|
|
565
|
+
projectName: account.projectName,
|
|
566
|
+
creator: account.creator.toString(),
|
|
567
|
+
registeredAt,
|
|
568
|
+
status: account.status,
|
|
569
|
+
merkleRoot: Buffer.from(account.merkleRoot).toString("hex"),
|
|
570
|
+
fileCount: account.fileCount,
|
|
571
|
+
repoUrl: account.repoUrl,
|
|
572
|
+
fileHashesHash: Buffer.from(account.fileHashesHash).toString("hex")
|
|
573
|
+
};
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.log(chalk3.gray(` Could not query Solana: ${err.message}`));
|
|
580
|
+
}
|
|
581
|
+
if (onChainRecord) {
|
|
582
|
+
const statusLabel = STATUS_LABELS[onChainRecord.status] || "Unknown";
|
|
583
|
+
console.log(chalk3.green("\n On-chain provenance found:\n"));
|
|
584
|
+
console.log(` ${chalk3.bold("Project:")} ${onChainRecord.projectName}`);
|
|
585
|
+
console.log(` ${chalk3.bold("Creator:")} ${onChainRecord.creator.substring(0, 8)}...${onChainRecord.creator.substring(onChainRecord.creator.length - 4)}`);
|
|
586
|
+
console.log(` ${chalk3.bold("Registered:")} ${new Date(onChainRecord.registeredAt * 1e3).toISOString().substring(0, 10)}`);
|
|
587
|
+
console.log(` ${chalk3.bold("Status:")} ${statusLabel}`);
|
|
588
|
+
console.log(` ${chalk3.bold("Files:")} ${onChainRecord.fileCount}`);
|
|
589
|
+
console.log(` ${chalk3.bold("Merkle Root:")} ${onChainRecord.merkleRoot.substring(0, 16)}...`);
|
|
590
|
+
} else {
|
|
591
|
+
console.log(chalk3.yellow("\n Warning: No provenance record found for this repository."));
|
|
592
|
+
console.log(chalk3.yellow(" Origin is unverified.\n"));
|
|
593
|
+
}
|
|
594
|
+
console.log(chalk3.gray(`
|
|
595
|
+
Cloning ${url}...`));
|
|
596
|
+
const cloneArgs = options.directory ? `${url} ${options.directory}` : url;
|
|
597
|
+
try {
|
|
598
|
+
execSync2(`git clone ${cloneArgs}`, { stdio: "inherit" });
|
|
599
|
+
} catch {
|
|
600
|
+
console.error(chalk3.red("\ngit clone failed."));
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
if (onChainRecord) {
|
|
604
|
+
console.log(chalk3.gray("\nVerifying cloned code against on-chain fingerprint..."));
|
|
605
|
+
const clonedFp = await fingerprintDirectory(targetDir);
|
|
606
|
+
if (clonedFp.merkleRoot === onChainRecord.merkleRoot) {
|
|
607
|
+
console.log(chalk3.green("\n Verified: code matches on-chain fingerprint."));
|
|
608
|
+
} else {
|
|
609
|
+
const fileHashesJson = JSON.stringify(
|
|
610
|
+
Object.fromEntries(
|
|
611
|
+
Object.entries(clonedFp.fileHashes).sort(([a], [b]) => a.localeCompare(b))
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
const clonedHashesHash = createHash4("sha256").update(fileHashesJson).digest("hex");
|
|
615
|
+
if (clonedHashesHash === onChainRecord.fileHashesHash) {
|
|
616
|
+
console.log(chalk3.green("\n Verified: file content matches on-chain commitment."));
|
|
617
|
+
} else {
|
|
618
|
+
console.log(chalk3.yellow("\n Warning: code does NOT match registered fingerprint."));
|
|
619
|
+
console.log(chalk3.yellow(" The repository may have been modified since registration."));
|
|
620
|
+
console.log(chalk3.gray(" Run 'gitchain verify' for detailed analysis."));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
console.log(chalk3.gray("\nInitializing GitChain in cloned repository..."));
|
|
625
|
+
const cloneConfig = {
|
|
626
|
+
projectName: onChainRecord?.projectName || repoName,
|
|
627
|
+
walletPath: DEFAULT_WALLET_PATH2,
|
|
628
|
+
rpcUrl: DEFAULT_RPC_URL2,
|
|
629
|
+
programId: DEFAULT_PROGRAM_ID2
|
|
630
|
+
};
|
|
631
|
+
writeConfig(targetDir, cloneConfig);
|
|
632
|
+
writeProvenance(targetDir, {
|
|
633
|
+
published: onChainRecord ? true : false,
|
|
634
|
+
merkleRoot: onChainRecord?.merkleRoot,
|
|
635
|
+
fileCount: onChainRecord?.fileCount,
|
|
636
|
+
publishedAt: onChainRecord ? new Date(onChainRecord.registeredAt * 1e3).toISOString() : void 0
|
|
637
|
+
});
|
|
638
|
+
ensureGitignore(targetDir);
|
|
639
|
+
console.log(chalk3.green(`
|
|
640
|
+
Done! Repository cloned to ${targetDir}`));
|
|
641
|
+
if (onChainRecord) {
|
|
642
|
+
console.log(chalk3.gray(" GitChain provenance data imported from Solana."));
|
|
643
|
+
} else {
|
|
644
|
+
console.log(chalk3.gray(" GitChain initialized (no provenance data available)."));
|
|
645
|
+
}
|
|
646
|
+
console.log();
|
|
647
|
+
}
|
|
648
|
+
function extractRepoName(url) {
|
|
649
|
+
const match = url.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
650
|
+
return match ? match[1] : basename2(url).replace(/\.git$/, "");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/commands/verify.ts
|
|
654
|
+
import chalk4 from "chalk";
|
|
655
|
+
import { resolve as resolve4 } from "node:path";
|
|
656
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
657
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
658
|
+
async function verifyCommand(targetPath, options) {
|
|
659
|
+
const resolvedTarget = resolve4(targetPath);
|
|
660
|
+
if (!existsSync4(resolvedTarget)) {
|
|
661
|
+
console.error(chalk4.red(`Error: Path not found: ${resolvedTarget}`));
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const configPath = resolve4(options.path || ".");
|
|
665
|
+
let rpcUrl = "https://api.devnet.solana.com";
|
|
666
|
+
let walletPath = process.platform === "win32" ? `${process.env.USERPROFILE}\\.config\\solana\\id.json` : `${process.env.HOME}/.config/solana/id.json`;
|
|
667
|
+
let programId = "";
|
|
668
|
+
if (isInitialized(configPath)) {
|
|
669
|
+
const config = readConfig(configPath);
|
|
670
|
+
rpcUrl = config.rpcUrl;
|
|
671
|
+
walletPath = config.walletPath;
|
|
672
|
+
programId = config.programId;
|
|
673
|
+
}
|
|
674
|
+
console.log(chalk4.gray("Fingerprinting target code..."));
|
|
675
|
+
const suspectFingerprint = await fingerprintDirectory(resolvedTarget);
|
|
676
|
+
console.log(chalk4.gray(` Files: ${suspectFingerprint.fileCount}`));
|
|
677
|
+
console.log(chalk4.gray(` Merkle root: ${suspectFingerprint.merkleRoot.substring(0, 16)}...`));
|
|
678
|
+
console.log(chalk4.gray("\nFetching on-chain lineage records..."));
|
|
679
|
+
const keypair = loadKeypair(walletPath);
|
|
680
|
+
const connection = getConnection(rpcUrl);
|
|
681
|
+
const program2 = getProgram(connection, keypair);
|
|
682
|
+
let allProjects = [];
|
|
683
|
+
try {
|
|
684
|
+
allProjects = await fetchAllProjectsViaProgram(program2);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
console.error(chalk4.red(`Failed to fetch on-chain records: ${err.message}`));
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
if (allProjects.length === 0) {
|
|
690
|
+
console.log(chalk4.yellow("\nNo projects registered on-chain yet."));
|
|
691
|
+
console.log(chalk4.gray("Register a project first with 'gitchain publish'."));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
console.log(chalk4.gray(` Found ${allProjects.length} registered project(s)
|
|
695
|
+
`));
|
|
696
|
+
let bestMatch = null;
|
|
697
|
+
for (const item of allProjects) {
|
|
698
|
+
const pubkey = item.publicKey ?? item.pubkey;
|
|
699
|
+
const account = item.account;
|
|
700
|
+
const onChainMerkle = Buffer.from(account.merkleRoot).toString("hex");
|
|
701
|
+
const merkleMatch = onChainMerkle === suspectFingerprint.merkleRoot;
|
|
702
|
+
let fileOverlapPercent = 0;
|
|
703
|
+
let confidence = 0;
|
|
704
|
+
if (merkleMatch) {
|
|
705
|
+
fileOverlapPercent = 100;
|
|
706
|
+
confidence = 100;
|
|
707
|
+
} else {
|
|
708
|
+
const suspectHashesHash = computeFileHashesHash(suspectFingerprint);
|
|
709
|
+
const onChainHashesHash = Buffer.from(account.fileHashesHash).toString("hex");
|
|
710
|
+
if (suspectHashesHash === onChainHashesHash) {
|
|
711
|
+
fileOverlapPercent = 100;
|
|
712
|
+
confidence = 99;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const overlapForRanking = merkleMatch ? 100 : fileOverlapPercent;
|
|
716
|
+
if (!bestMatch || overlapForRanking > bestMatch.fileOverlapPercent) {
|
|
717
|
+
bestMatch = {
|
|
718
|
+
projectName: account.projectName,
|
|
719
|
+
creator: account.creator.toString(),
|
|
720
|
+
registeredAt: account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt),
|
|
721
|
+
merkleMatch,
|
|
722
|
+
fileOverlapPercent: overlapForRanking,
|
|
723
|
+
confidence: merkleMatch ? 100 : confidence,
|
|
724
|
+
status: merkleMatch ? "Disputed Origin" : fileOverlapPercent >= 90 ? "Disputed Origin" : "Checking...",
|
|
725
|
+
repoUrl: account.repoUrl || "",
|
|
726
|
+
pda: pubkey.toString()
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (bestMatch && bestMatch.fileOverlapPercent === 0) {
|
|
731
|
+
console.log(chalk4.gray(" No Merkle or commitment match. Trying local comparison...\n"));
|
|
732
|
+
if (isInitialized(configPath)) {
|
|
733
|
+
const localFp = await fingerprintDirectory(configPath);
|
|
734
|
+
const comparison = compareFingerprints(localFp, suspectFingerprint);
|
|
735
|
+
if (comparison.fileOverlapPercent > (bestMatch?.fileOverlapPercent || 0)) {
|
|
736
|
+
const config = readConfig(configPath);
|
|
737
|
+
bestMatch = {
|
|
738
|
+
projectName: config.projectName,
|
|
739
|
+
creator: keypair.publicKey.toString(),
|
|
740
|
+
registeredAt: Date.now() / 1e3,
|
|
741
|
+
merkleMatch: comparison.merkleMatch,
|
|
742
|
+
fileOverlapPercent: comparison.fileOverlapPercent,
|
|
743
|
+
confidence: comparison.confidence,
|
|
744
|
+
status: comparison.status,
|
|
745
|
+
repoUrl: "",
|
|
746
|
+
pda: bestMatch?.pda || "local"
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
console.log(chalk4.bold("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
752
|
+
console.log(chalk4.bold("\u2551 Lineage Analysis Result \u2551"));
|
|
753
|
+
console.log(chalk4.bold("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n"));
|
|
754
|
+
if (!bestMatch || bestMatch.fileOverlapPercent === 0) {
|
|
755
|
+
console.log(chalk4.green(" Status: No known lineage match found."));
|
|
756
|
+
console.log(chalk4.gray(" This code does not match any registered project."));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
console.log(` ${chalk4.bold("Closest Known Origin:")} ${bestMatch.projectName}`);
|
|
760
|
+
console.log(` ${chalk4.bold("Creator:")} ${bestMatch.creator.substring(0, 8)}...${bestMatch.creator.substring(bestMatch.creator.length - 4)}`);
|
|
761
|
+
console.log(` ${chalk4.bold("Registered:")} ${new Date(bestMatch.registeredAt * 1e3).toISOString().substring(0, 10)}`);
|
|
762
|
+
if (bestMatch.repoUrl) {
|
|
763
|
+
console.log(` ${chalk4.bold("Repo:")} ${bestMatch.repoUrl}`);
|
|
764
|
+
}
|
|
765
|
+
console.log(chalk4.bold("\n Signals:"));
|
|
766
|
+
console.log(` Exact File Overlap: ${formatPercent(bestMatch.fileOverlapPercent)}`);
|
|
767
|
+
console.log(` Merkle Root Match: ${bestMatch.merkleMatch ? chalk4.red("Yes (exact copy)") : "No"}`);
|
|
768
|
+
console.log(chalk4.bold("\n Conclusion:"));
|
|
769
|
+
const statusColor = bestMatch.fileOverlapPercent >= 60 ? chalk4.red : chalk4.yellow;
|
|
770
|
+
console.log(` ${chalk4.bold("Status:")} ${statusColor(bestMatch.status)}`);
|
|
771
|
+
console.log(` ${chalk4.bold("Confidence:")} ${bestMatch.confidence}%
|
|
772
|
+
`);
|
|
773
|
+
}
|
|
774
|
+
function formatPercent(value) {
|
|
775
|
+
const formatted = `${value.toFixed(1)}%`;
|
|
776
|
+
if (value >= 90) return chalk4.red(formatted);
|
|
777
|
+
if (value >= 60) return chalk4.yellow(formatted);
|
|
778
|
+
if (value >= 30) return chalk4.cyan(formatted);
|
|
779
|
+
return chalk4.green(formatted);
|
|
780
|
+
}
|
|
781
|
+
function computeFileHashesHash(fp) {
|
|
782
|
+
const sorted = JSON.stringify(
|
|
783
|
+
Object.fromEntries(
|
|
784
|
+
Object.entries(fp.fileHashes).sort(([a], [b]) => a.localeCompare(b))
|
|
785
|
+
)
|
|
786
|
+
);
|
|
787
|
+
return createHash5("sha256").update(sorted).digest("hex");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/commands/status.ts
|
|
791
|
+
import chalk5 from "chalk";
|
|
792
|
+
import { resolve as resolve5 } from "node:path";
|
|
793
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
794
|
+
async function statusCommand(options) {
|
|
795
|
+
const projectPath = resolve5(options.path || ".");
|
|
796
|
+
if (!isInitialized(projectPath)) {
|
|
797
|
+
console.error(chalk5.red("Error: Not initialized. Run 'gitchain init' first."));
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const config = readConfig(projectPath);
|
|
801
|
+
const provenance = readProvenance(projectPath);
|
|
802
|
+
console.log(chalk5.bold(`
|
|
803
|
+
GitChain Status: ${config.projectName}
|
|
804
|
+
`));
|
|
805
|
+
if (!provenance.published) {
|
|
806
|
+
console.log(chalk5.yellow(" Not published."));
|
|
807
|
+
console.log(chalk5.gray(" Run 'gitchain publish' to register on Solana.\n"));
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
console.log(chalk5.gray(" Local State:"));
|
|
811
|
+
console.log(` Merkle Root: ${provenance.merkleRoot?.substring(0, 32)}...`);
|
|
812
|
+
console.log(` Files: ${provenance.fileCount}`);
|
|
813
|
+
console.log(` Published: ${provenance.publishedAt}`);
|
|
814
|
+
console.log(` PDA: ${provenance.pdaAddress}`);
|
|
815
|
+
console.log(` TX: ${provenance.txSignature}`);
|
|
816
|
+
if (existsSync5(config.walletPath)) {
|
|
817
|
+
console.log(chalk5.gray("\n On-Chain State:"));
|
|
818
|
+
try {
|
|
819
|
+
const keypair = loadKeypair(config.walletPath);
|
|
820
|
+
const connection = getConnection(config.rpcUrl);
|
|
821
|
+
const program2 = getProgram(connection, keypair);
|
|
822
|
+
const [pda] = deriveProjectPda(keypair.publicKey, config.projectName);
|
|
823
|
+
const account = await program2.account.projectRecord.fetch(pda);
|
|
824
|
+
const statusLabel = STATUS_LABELS[account.status] || `Unknown (${account.status})`;
|
|
825
|
+
const registeredAt = account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt);
|
|
826
|
+
console.log(` Creator: ${account.creator.toString()}`);
|
|
827
|
+
console.log(` Status: ${statusLabel}`);
|
|
828
|
+
console.log(` Registered: ${new Date(registeredAt * 1e3).toISOString()}`);
|
|
829
|
+
console.log(` Merkle Root: ${Buffer.from(account.merkleRoot).toString("hex").substring(0, 32)}...`);
|
|
830
|
+
console.log(` Files: ${account.fileCount}`);
|
|
831
|
+
if (account.repoUrl) {
|
|
832
|
+
console.log(` Repo: ${account.repoUrl}`);
|
|
833
|
+
}
|
|
834
|
+
console.log(
|
|
835
|
+
chalk5.gray(`
|
|
836
|
+
Explorer: https://explorer.solana.com/address/${pda.toString()}?cluster=devnet`)
|
|
837
|
+
);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
console.log(chalk5.yellow(` Could not fetch on-chain data: ${err.message}`));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
console.log();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/index.ts
|
|
846
|
+
var program = new Command();
|
|
847
|
+
program.name("gitchain").description("Software provenance & lineage protocol on Solana").version("0.1.0");
|
|
848
|
+
program.command("init").description("Initialize GitChain tracking for this repository").option("-p, --path <path>", "Path to Git repository", ".").action(initCommand);
|
|
849
|
+
program.command("publish <remote> <branch>").description("Fingerprint, register on Solana, then push to remote").option("-p, --path <path>", "Path to Git repository", ".").action(publishCommand);
|
|
850
|
+
program.command("clone <url>").description("Check on-chain provenance, then clone repository").option("-d, --directory <dir>", "Target directory for clone").action(cloneCommand);
|
|
851
|
+
program.command("verify <target-path>").description("Verify code against on-chain lineage records").option("-p, --path <path>", "Path to initialized GitChain project for config", ".").action(verifyCommand);
|
|
852
|
+
program.command("status").description("Show GitChain provenance status").option("-p, --path <path>", "Path to GitChain project", ".").action(statusCommand);
|
|
853
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitchain-sol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Software provenance & lineage protocol on Solana — fingerprint, register, and verify code on-chain",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitchain": "dist/gitchain.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/gitchain.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"build": "tsc && node build.mjs",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"solana",
|
|
20
|
+
"provenance",
|
|
21
|
+
"git",
|
|
22
|
+
"blockchain",
|
|
23
|
+
"fingerprint",
|
|
24
|
+
"lineage",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"author": "ayushsaklani",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/ayushsaklani/gitchain"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@coral-xyz/anchor": "^0.32.1",
|
|
35
|
+
"@solana/web3.js": "^1.98.4",
|
|
36
|
+
"chalk": "^5.6.2",
|
|
37
|
+
"commander": "^14.0.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@gitchain/fingerprint": "*",
|
|
41
|
+
"@types/node": "^25.5.0",
|
|
42
|
+
"esbuild": "^0.27.5"
|
|
43
|
+
}
|
|
44
|
+
}
|