Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# TrustChain Backend Environment Variables

# Solana RPC URL
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com

# Notary Secret Key (JSON Array of numbers or Hex String)
# CRITICAL: Do not commit the actual secret key to the repository!
NOTARY_SECRET=[1,2,3,...]
7 changes: 5 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
"start": "node server.js",
"dev": "node server.js",
"test": "node test.js && node tests/security.test.js",
"test:engine": "node test.js"
"test:engine": "node test.js",
"sync:notary": "tsx services/notary_sync.ts"
},
"dependencies": {
"@coral-xyz/anchor": "^0.29.0",
"@solana/web3.js": "^1.98.4",
"axios": "^1.6.8",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^4.18.2"
"express": "^4.18.2",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@types/node": "^20.0.0",
Expand Down
174 changes: 174 additions & 0 deletions backend/services/notary_sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
Connection,
Keypair,
PublicKey,
Transaction,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";
import crypto from "crypto";
import dotenv from "dotenv";

dotenv.config();

// Mock data
const MOCK_WALLET = new PublicKey("11111111111111111111111111111111");

// Load Notary Secret from Environment Variable
if (!process.env.NOTARY_SECRET) {
throw new Error("NOTARY_SECRET environment variable not set. Please set it in .env file.");
}

// Support both JSON array format and Hex string format for convenience
let notarySecret: Uint8Array;
try {
const parsed = JSON.parse(process.env.NOTARY_SECRET);
if (Array.isArray(parsed)) {
notarySecret = new Uint8Array(parsed);
Comment on lines +19 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Tighten NOTARY_SECRET validation to avoid ambiguous or malformed key material

Currently any hex string is accepted and only fails later inside Keypair.fromSecretKey if the length is wrong. Consider checking that the decoded Uint8Array has the expected length (e.g., 64 bytes) before constructing the keypair, and throwing a clear error when it doesn’t.

Suggested implementation:

dotenv.config();

const NOTARY_SECRET_KEY_LENGTH = 64;

// Mock data
const MOCK_WALLET = new PublicKey("11111111111111111111111111111111");
    if (/^[0-9a-fA-F]+$/.test(cleanHex)) {
         notarySecret = Uint8Array.from(Buffer.from(cleanHex, 'hex'));
    } else {
        throw new Error("Invalid NOTARY_SECRET format. Must be JSON array of numbers or Hex string.");
    }
}

if (!notarySecret || notarySecret.length !== NOTARY_SECRET_KEY_LENGTH) {
    throw new Error(
        `NOTARY_SECRET must decode to a ${NOTARY_SECRET_KEY_LENGTH}-byte secret key; got ${notarySecret ? notarySecret.length : 0} bytes.`
    );
}

const NOTARY_KEYPAIR = Keypair.fromSecretKey(notarySecret);

} else {
throw new Error("NOTARY_SECRET JSON is not an array");
}
} catch (e) {
// If not JSON, try Hex
// Remove '0x' prefix if present
const cleanHex = process.env.NOTARY_SECRET.replace(/^0x/, '');
if (/^[0-9a-fA-F]+$/.test(cleanHex)) {
notarySecret = Uint8Array.from(Buffer.from(cleanHex, 'hex'));
} else {
throw new Error("Invalid NOTARY_SECRET format. Must be JSON array of numbers or Hex string.");
}
}

const NOTARY_KEYPAIR = Keypair.fromSecretKey(notarySecret);

// Placeholder Program ID from lib.rs
const PROGRAM_ID = new PublicKey("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

interface IntegrityScore {
gini: number;
hhi: number;
status: string;
}

// Mock function to fetch score from Integrity Engine
async function fetchVerifiedScore(wallet: PublicKey): Promise<IntegrityScore> {
// In reality, this would call backend/integrityEngine.js logic
console.log(`Fetching score for ${wallet.toBase58()}...`);
return {
gini: 0.25,
hhi: 0.15,
status: "VERIFIED",
};
}

async function updateOnChainPDA(wallet: PublicKey, score: IntegrityScore) {
// Connect to devnet (or localnet)
const connection = new Connection("https://api.devnet.solana.com", "confirmed");

// PDA derivation: seeds = [b"integrity", user.key().as_ref()]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Align PDA seed comment with actual implementation (config vs integrity)

The inline comment still refers to [b"integrity", user.key().as_ref()], but the actual PDA uses ["config", wallet]. Please update the comment to match the real seeds ("config" + wallet) to avoid confusion when working with the PDA or comparing against the on-chain Anchor account definition.

Suggested change
// PDA derivation: seeds = [b"integrity", user.key().as_ref()]
// PDA derivation: seeds = [b"config", wallet.toBuffer()]

const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("config"), wallet.toBuffer()],
PROGRAM_ID
);

console.log(`PDA for wallet ${wallet.toBase58()}: ${pda.toBase58()}`);

// Calculate Discriminator for "global:update_integrity"
// Anchor uses sha256("global:<instruction_name>") and takes first 8 bytes
const discriminator = crypto.createHash("sha256").update("global:update_integrity").digest().subarray(0, 8);

const giniBuffer = Buffer.alloc(2);
giniBuffer.writeUInt16LE(Math.floor(score.gini * 10000)); // Scale by 10000

const hhiBuffer = Buffer.alloc(2);
hhiBuffer.writeUInt16LE(Math.floor(score.hhi * 10000)); // Scale by 10000

const statusBuffer = Buffer.alloc(1);
// status: VERIFIED = 1, PROBATIONARY = 2, ERROR = 0 (Example mapping)
let statusVal = 0;
if (score.status === 'VERIFIED') statusVal = 1;
else if (score.status === 'PROBATIONARY') statusVal = 2;

statusBuffer.writeUInt8(statusVal);

const data = Buffer.concat([discriminator, giniBuffer, hhiBuffer, statusBuffer]);

// Construct instruction
// Accounts must match the UpdateIntegrity struct in lib.rs:
// 1. user_integrity (PDA, mut)
// 2. user (Unchecked, not mut)
// 3. notary (Signer, mut)
// 4. system_program
const instruction = new TransactionInstruction({
keys: [
{ pubkey: pda, isSigner: false, isWritable: true }, // user_integrity
{ pubkey: wallet, isSigner: false, isWritable: false }, // user
{ pubkey: NOTARY_KEYPAIR.publicKey, isSigner: true, isWritable: true }, // notary
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
],
programId: PROGRAM_ID,
data: data,
});

const transaction = new Transaction().add(instruction);

// Sign transaction (Offline signing simulation)
// We need a recent blockhash to sign.
try {
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
} catch (err) {
console.warn("Could not fetch blockhash (network issue?), using mock blockhash for simulation.");
transaction.recentBlockhash = Keypair.generate().publicKey.toBase58(); // Mock blockhash
}

transaction.feePayer = NOTARY_KEYPAIR.publicKey;
transaction.sign(NOTARY_KEYPAIR);

// ---------------------------------------------------------
// Ed25519 Verification Step
// ---------------------------------------------------------

// 1. Verify that the transaction object itself considers the signatures valid
if (!transaction.verifySignatures()) {
throw new Error("Transaction signature verification failed!");
}

// 2. Explicitly verify using nacl to ensure the Notary signed it
const signatureObj = transaction.signatures.find(s => s.publicKey.equals(NOTARY_KEYPAIR.publicKey));

if (!signatureObj || !signatureObj.signature) {
throw new Error("Notary signature missing from transaction");
}

const message = transaction.serializeMessage();
const isValid = nacl.sign.detached.verify(
message,
signatureObj.signature,
NOTARY_KEYPAIR.publicKey.toBuffer()
);

if (isValid) {
console.log("✅ Ed25519 Signature Verified: Update authorized by TrustChain Notary.");
console.log("Notary Public Key:", NOTARY_KEYPAIR.publicKey.toBase58());
console.log("Signature:", Buffer.from(signatureObj.signature).toString('hex'));
} else {
console.error("❌ Invalid Signature: Unauthorized update attempt.");
throw new Error("Security Violation: Invalid Signature");
}

console.log("Transaction successfully constructed with correct discriminator and accounts.");
}

async function main() {
try {
const score = await fetchVerifiedScore(MOCK_WALLET);
await updateOnChainPDA(MOCK_WALLET, score);
} catch (e) {
console.error("Error during notary sync:", e);
process.exit(1);
}
}

main();
78 changes: 78 additions & 0 deletions docs/FAIRSCALE_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# FairScale API Documentation (Reconstructed)

**Note:** This documentation is reconstructed from the project context and `README.md` as the original source (`https://docs.fairscale.xyz/`) was inaccessible during the environment setup.

## Overview

FairScale provides a reputation tiering system for wallet addresses, allowing protocols to assess the long-term behavior and fairness of liquidity providers and traders.

## Endpoints

### Get Wallet Reputation

Retrieves the reputation tier and score for a specific wallet address.

- **URL:** `https://api.fairscale.xyz/v1/reputation/:address` (Hypothetical endpoint)
- **Method:** `GET`
- **Params:**
- `address`: The Solana wallet address (Base58 encoded).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Consider hyphenating "Base58 encoded" to "Base58-encoded" for grammatical correctness.

Because it modifies “wallet address” as a compound adjective, the hyphenated form is preferred.

Suggested change
- `address`: The Solana wallet address (Base58 encoded).
- `address`: The Solana wallet address (Base58-encoded).


#### Response

```json
{
"address": "WalletAddressHere",
"tier": 3,
"score": 85,
"lastUpdated": "2023-10-27T10:00:00Z"
}
```

- `tier`: Integer (1-5). Higher is better.
- Tier 1: New / Low Reputation (High Risk)
- Tier 2: Probationary
- Tier 3: Verified / Trusted
- Tier 4: High Reputation
- Tier 5: Elite
- `score`: Integer (0-100). Granular reputation score.

## Integration Logic

TrustChain integrates FairScale with its internal Gini coefficient analysis to determine eligibility for rewards and protected swaps.

### Logic Flow

1. **Query FairScale API** to get the `tier`.
2. **Calculate Gini Coefficient** locally or via TrustChain backend (`/api/verify`).
3. **Apply Gatekeeper Logic**:

```javascript
const GINI_THRESHOLD = 0.5; // TrustChain Probationary Threshold
const FAIRSCALE_TIER_THRESHOLD = 2; // Minimum Tier required

if (giniScore >= GINI_THRESHOLD || fairScoreTier < FAIRSCALE_TIER_THRESHOLD) {
return "BLOCKED";
}
return "ALLOWED";
```

### Pseudocode Reference

From `README.md`:

```python
async def check_lp_eligibility(wallet_address, wallet_trades):
fairscore_tier = await fairscale_api(wallet_address)
gini_score = calculate_gini_coefficient(wallet_trades)

if gini_score > 0.3 or fairscore_tier < 2:
return "SYBIL_BLOCKED - Ineligible for LP rewards"
return "LP_REWARD_ELIGIBLE - Fair provider verified"
```

## Environment Variables

Ensure the following are configured in your environment:

- `FAIRSCALE_API_KEY`: API Key for accessing FairScale services (if required).
- `FAIRSCALE_API_URL`: Base URL for the API (default: `https://api.fairscale.xyz`).
48 changes: 48 additions & 0 deletions frontend/src/components/Guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { useIntegrity } from '../hooks/useIntegrity';
import { PROBATIONARY_THRESHOLD, SYBIL_STATUS } from '../constants/integrity';

interface GuardProps {
children: React.ReactNode;
fallback?: React.ReactNode;
threshold?: number;
sybilStatus?: string;
}

export const Guard: React.FC<GuardProps> = ({
children,
fallback,
threshold = PROBATIONARY_THRESHOLD,
sybilStatus = SYBIL_STATUS
}) => {
const { giniScore, loading, error, status } = useIntegrity();

if (loading) {
return <div className="p-4 text-center">Verifying integrity...</div>;
}

if (error) {
return <div className="p-4 text-red-500">Error verifying wallet: {error}</div>;
}

// Logic: Block if Gini > threshold or status is explicitly sybilStatus
const isSybil = (giniScore !== null && giniScore > threshold) || status === sybilStatus;

if (isSybil) {
if (fallback) {
return <>{fallback}</>;
}
return (
<div className="guard-blocked p-4 bg-red-100 text-red-800 rounded-lg border border-red-200">
<h3 className="font-bold text-lg mb-2">Protected Swap: Access Denied</h3>
<p>Your wallet integrity score indicates high risk (Sybil detected).</p>
<div className="mt-2 text-sm">
<span>Gini Score: {giniScore?.toFixed(3)}</span>
<span className="ml-2">(Threshold: {threshold})</span>
</div>
</div>
);
}

return <>{children}</>;
};
2 changes: 2 additions & 0 deletions frontend/src/constants/integrity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const PROBATIONARY_THRESHOLD = 0.5;
export const SYBIL_STATUS = 'SYBIL';
Loading