-
Notifications
You must be signed in to change notification settings - Fork 0
Sovereign V2 Genesis Push #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fairscale-integration
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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,...] |
| 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); | ||||||
| } 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()] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (typo): Align PDA seed comment with actual implementation ( The inline comment still refers to
Suggested change
|
||||||
| 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(); | ||||||
| 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). | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
|
|
||||||
| #### 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`). | ||||||
| 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}</>; | ||
| }; |
| 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'; |
There was a problem hiding this comment.
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.fromSecretKeyif the length is wrong. Consider checking that the decodedUint8Arrayhas the expected length (e.g., 64 bytes) before constructing the keypair, and throwing a clear error when it doesn’t.Suggested implementation: