TrustChain On-Chain Integrity Protocol#12
Conversation
- Initialize Anchor project in `on-chain/trustchain-notary` - Implement `UserIntegrity` account and `update_integrity` instruction with Notary access control - Create `backend/services/notary_sync.ts` for signing and verifying updates - Add `sync:notary` script and dependencies to backend - Secure secret management via environment variables Co-authored-by: Freedomwithin <166790647+Freedomwithin@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideImplements a new on-chain TrustChain notary program using Anchor, plus a backend notary sync service that derives the PDA, encodes the integrity update instruction, signs and verifies transactions with an env-configured notary key, and wires the necessary dependencies and configuration. Sequence diagram for backend notary sync updating on-chain integritysequenceDiagram
actor Operator
participant NotarySyncService
participant IntegrityEngine
participant SolanaRPC
participant TrustchainNotaryProgram
participant UserIntegrityAccount
Operator->>NotarySyncService: run sync:notary
NotarySyncService->>NotarySyncService: load NOTARY_SECRET
NotarySyncService->>IntegrityEngine: fetchVerifiedScore(wallet)
IntegrityEngine-->>NotarySyncService: IntegrityScore(gini,hhi,status)
NotarySyncService->>NotarySyncService: derive PDA with seeds [b("integrity"), wallet]
NotarySyncService->>NotarySyncService: build update_integrity instruction data
NotarySyncService->>SolanaRPC: getLatestBlockhash()
SolanaRPC-->>NotarySyncService: blockhash
NotarySyncService->>NotarySyncService: construct Transaction
NotarySyncService->>NotarySyncService: sign with NOTARY_KEYPAIR
NotarySyncService->>NotarySyncService: transaction.verifySignatures()
NotarySyncService->>NotarySyncService: nacl.sign.detached.verify(message, signature, notary_pubkey)
NotarySyncService->>SolanaRPC: sendTransaction(transaction)
SolanaRPC->>TrustchainNotaryProgram: invoke update_integrity
TrustchainNotaryProgram->>TrustchainNotaryProgram: check notary signer == NOTARY_PUBKEY
TrustchainNotaryProgram->>UserIntegrityAccount: init_if_needed and update fields
TrustchainNotaryProgram-->>SolanaRPC: success
SolanaRPC-->>NotarySyncService: transaction confirmation
Updated class diagram for TrustChain notary program and notary sync serviceclassDiagram
class trustchain_notary_program {
<<program>>
+update_integrity(ctx: UpdateIntegrity, gini_score: u16, hhi_score: u16, status: u8) Result
}
class UpdateIntegrity {
+user_integrity: UserIntegrity
+user: UncheckedAccount
+notary: Signer
+system_program: System
}
class UserIntegrity {
+gini_score: u16
+hhi_score: u16
+status: u8
+last_updated: i64
}
class TrustChainError {
<<enum>>
+UnauthorizedNotary
}
class IntegrityScore {
+gini: number
+hhi: number
+status: string
}
class NotarySyncService {
+MOCK_WALLET: PublicKey
+NOTARY_KEYPAIR: Keypair
+PROGRAM_ID: PublicKey
+fetchVerifiedScore(wallet: PublicKey) Promise~IntegrityScore~
+updateOnChainPDA(wallet: PublicKey, score: IntegrityScore) Promise~void~
+main() Promise~void~
}
class EnvConfig {
+NOTARY_SECRET: string
+loadNotarySecret() Uint8Array
}
class SolanaConnection {
+getLatestBlockhash() Promise
+sendTransaction(transaction: Transaction) Promise
}
class TransactionBuilder {
+derivePDA(wallet: PublicKey, programId: PublicKey) PublicKey
+buildUpdateIntegrityData(gini: number, hhi: number, status: string) Buffer
+buildInstruction(pda: PublicKey, wallet: PublicKey, notaryPubkey: PublicKey, programId: PublicKey, data: Buffer) TransactionInstruction
+signAndVerify(transaction: Transaction, notaryKeypair: Keypair) boolean
}
trustchain_notary_program --> UpdateIntegrity
UpdateIntegrity --> UserIntegrity
trustchain_notary_program --> TrustChainError
NotarySyncService --> IntegrityScore
NotarySyncService --> SolanaConnection
NotarySyncService --> TransactionBuilder
NotarySyncService --> EnvConfig
TransactionBuilder --> UserIntegrity
EnvConfig --> NotarySyncService
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- Loading and validating
NOTARY_SECRETat module top-level will throw as soon as the file is imported; consider moving this parsing/validation intomain()(or a dedicated init function) so that other code can importnotary_sync.tswithout crashing when the env var is missing or misconfigured. - When parsing
NOTARY_SECRETintonotarySecret, it would be safer to assert the expected length (e.g., 64-byte Ed25519 secret key) and error with a clear message if it doesn't match, to avoid subtle misconfigurations being accepted. - The
ConnectioninupdateOnChainPDAis hardcoded tohttps://api.devnet.solana.com; consider sourcing the RPC endpoint and commitment level from configuration (e.g., env vars) so this script can be reused across clusters/environments without code changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Loading and validating `NOTARY_SECRET` at module top-level will throw as soon as the file is imported; consider moving this parsing/validation into `main()` (or a dedicated init function) so that other code can import `notary_sync.ts` without crashing when the env var is missing or misconfigured.
- When parsing `NOTARY_SECRET` into `notarySecret`, it would be safer to assert the expected length (e.g., 64-byte Ed25519 secret key) and error with a clear message if it doesn't match, to avoid subtle misconfigurations being accepted.
- The `Connection` in `updateOnChainPDA` is hardcoded to `https://api.devnet.solana.com`; consider sourcing the RPC endpoint and commitment level from configuration (e.g., env vars) so this script can be reused across clusters/environments without code changes.
## Individual Comments
### Comment 1
<location> `backend/services/notary_sync.ts:67` </location>
<code_context>
+
+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()]
</code_context>
<issue_to_address>
**suggestion:** Hardcoding the devnet RPC URL reduces flexibility across environments.
This ties the function to a single devnet endpoint and commitment. Consider reading the RPC URL (and optionally commitment) from an environment variable with a reasonable default so the same code can be used across devnet, localnet, mainnet, or other providers without code changes.
Suggested implementation:
```typescript
async function updateOnChainPDA(wallet: PublicKey, score: IntegrityScore) {
// Connect to Solana RPC (configurable via env; defaults to devnet)
const rpcUrl =
process.env.SOLANA_RPC_URL ?? "https://api.devnet.solana.com";
const commitment =
(process.env.SOLANA_COMMITMENT as Commitment | undefined) ?? "confirmed";
const connection = new Connection(rpcUrl, commitment);
```
1. Ensure `Commitment` is imported from `@solana/web3.js` at the top of `backend/services/notary_sync.ts`, e.g.:
`import { Connection, PublicKey, Commitment } from "@solana/web3.js";`
2. Optionally document the new environment variables (`SOLANA_RPC_URL`, `SOLANA_COMMITMENT`) in your configuration / README so deployments know how to target devnet, localnet, or mainnet without code changes.
</issue_to_address>
### Comment 2
<location> `backend/services/notary_sync.ts:121-123` </location>
<code_context>
+ 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
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Using a random public key as a mock blockhash can be misleading and produces an invalid on-chain transaction.
In the catch branch, we fall back to a random public key as the blockhash but still proceed and log a successful transaction. That transaction will never be valid on-chain and can mask underlying network failures. Consider either failing fast and surfacing the `getLatestBlockhash` error, or explicitly gating this behavior behind a dedicated simulation/offline mode so it can’t be mistaken for a sendable transaction.
</issue_to_address>
### Comment 3
<location> `on-chain/trustchain-notary/programs/trustchain-notary/src/lib.rs:6` </location>
<code_context>
+declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
+
+// Hardcoded Notary Public Key
+pub const NOTARY_PUBKEY: Pubkey = pubkey!("Eg39fu7jw4ynYg3N3mzqusb1qLEZBH2ov62T564BFdYF");
+
+#[program]
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Hardcoded NOTARY_PUBKEY risks drifting from the backend's NOTARY_SECRET configuration.
Because the program hardcodes `NOTARY_PUBKEY` while the backend derives its keypair from `NOTARY_SECRET`, any divergence (e.g. key rotation or env mismatch) will cause opaque `UnauthorizedNotary` failures on-chain. Consider either (a) documenting/automating how these values stay in sync, or (b) adding an on-chain config or deployment-time assertion that verifies the backend’s notary public key matches this constant.
Suggested implementation:
```rust
#[doc = "Hardcoded Notary public key.\n\n\
This MUST stay in sync with the backend's NOTARY_SECRET-derived public key.\n\
Deployment and CI pipelines should call the `assert_notary_pubkey` instruction\n\
with the backend-derived notary public key to fail fast if there is any\n\
mismatch, rather than surfacing opaque `UnauthorizedNotary` errors at runtime."]
pub const NOTARY_PUBKEY: Pubkey = pubkey!("Eg39fu7jw4ynYg3N3mzqusb1qLEZBH2ov62T564BFdYF");
```
```rust
#[program]
pub mod trustchain_notary {
use super::*;
/// Deployment-time assertion to ensure the backend's NOTARY_SECRET-derived
/// public key matches the on-chain NOTARY_PUBKEY constant.
///
/// Intended usage:
/// - Backend derives its notary keypair from NOTARY_SECRET.
/// - Deployment / CI script calls this instruction once, passing the
/// derived public key as `expected_notary`.
/// - If the keys differ, this transaction fails fast with
/// `UnauthorizedNotary`, preventing a misconfigured deployment.
pub fn assert_notary_pubkey(
_ctx: Context<AssertNotaryPubkey>,
expected_notary: Pubkey,
) -> Result<()> {
require_keys_eq!(expected_notary, NOTARY_PUBKEY, TrustChainError::UnauthorizedNotary);
Ok(())
}
pub fn update_integrity(ctx: Context<UpdateIntegrity>, gini_score: u16, hhi_score: u16, status: u8) -> Result<()> {
```
To fully wire this up, add the corresponding accounts struct somewhere in the file (e.g. near your other `#[derive(Accounts)]` definitions):
```rust
#[derive(Accounts)]
pub struct AssertNotaryPubkey {}
```
This empty accounts struct keeps the instruction simple (no runtime dependencies) while allowing off-chain deployment or CI tooling to submit a transaction that verifies the backend’s NOTARY_SECRET-derived public key matches `NOTARY_PUBKEY`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| async function updateOnChainPDA(wallet: PublicKey, score: IntegrityScore) { | ||
| // Connect to devnet (or localnet) | ||
| const connection = new Connection("https://api.devnet.solana.com", "confirmed"); |
There was a problem hiding this comment.
suggestion: Hardcoding the devnet RPC URL reduces flexibility across environments.
This ties the function to a single devnet endpoint and commitment. Consider reading the RPC URL (and optionally commitment) from an environment variable with a reasonable default so the same code can be used across devnet, localnet, mainnet, or other providers without code changes.
Suggested implementation:
async function updateOnChainPDA(wallet: PublicKey, score: IntegrityScore) {
// Connect to Solana RPC (configurable via env; defaults to devnet)
const rpcUrl =
process.env.SOLANA_RPC_URL ?? "https://api.devnet.solana.com";
const commitment =
(process.env.SOLANA_COMMITMENT as Commitment | undefined) ?? "confirmed";
const connection = new Connection(rpcUrl, commitment);- Ensure
Commitmentis imported from@solana/web3.jsat the top ofbackend/services/notary_sync.ts, e.g.:
import { Connection, PublicKey, Commitment } from "@solana/web3.js"; - Optionally document the new environment variables (
SOLANA_RPC_URL,SOLANA_COMMITMENT) in your configuration / README so deployments know how to target devnet, localnet, or mainnet without code changes.
| } catch (err) { | ||
| console.warn("Could not fetch blockhash (network issue?), using mock blockhash for simulation."); | ||
| transaction.recentBlockhash = Keypair.generate().publicKey.toBase58(); // Mock blockhash |
There was a problem hiding this comment.
issue (bug_risk): Using a random public key as a mock blockhash can be misleading and produces an invalid on-chain transaction.
In the catch branch, we fall back to a random public key as the blockhash but still proceed and log a successful transaction. That transaction will never be valid on-chain and can mask underlying network failures. Consider either failing fast and surfacing the getLatestBlockhash error, or explicitly gating this behavior behind a dedicated simulation/offline mode so it can’t be mistaken for a sendable transaction.
| declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); | ||
|
|
||
| // Hardcoded Notary Public Key | ||
| pub const NOTARY_PUBKEY: Pubkey = pubkey!("Eg39fu7jw4ynYg3N3mzqusb1qLEZBH2ov62T564BFdYF"); |
There was a problem hiding this comment.
suggestion (bug_risk): Hardcoded NOTARY_PUBKEY risks drifting from the backend's NOTARY_SECRET configuration.
Because the program hardcodes NOTARY_PUBKEY while the backend derives its keypair from NOTARY_SECRET, any divergence (e.g. key rotation or env mismatch) will cause opaque UnauthorizedNotary failures on-chain. Consider either (a) documenting/automating how these values stay in sync, or (b) adding an on-chain config or deployment-time assertion that verifies the backend’s notary public key matches this constant.
Suggested implementation:
#[doc = "Hardcoded Notary public key.\n\n\
This MUST stay in sync with the backend's NOTARY_SECRET-derived public key.\n\
Deployment and CI pipelines should call the `assert_notary_pubkey` instruction\n\
with the backend-derived notary public key to fail fast if there is any\n\
mismatch, rather than surfacing opaque `UnauthorizedNotary` errors at runtime."]
pub const NOTARY_PUBKEY: Pubkey = pubkey!("Eg39fu7jw4ynYg3N3mzqusb1qLEZBH2ov62T564BFdYF");#[program]
pub mod trustchain_notary {
use super::*;
/// Deployment-time assertion to ensure the backend's NOTARY_SECRET-derived
/// public key matches the on-chain NOTARY_PUBKEY constant.
///
/// Intended usage:
/// - Backend derives its notary keypair from NOTARY_SECRET.
/// - Deployment / CI script calls this instruction once, passing the
/// derived public key as `expected_notary`.
/// - If the keys differ, this transaction fails fast with
/// `UnauthorizedNotary`, preventing a misconfigured deployment.
pub fn assert_notary_pubkey(
_ctx: Context<AssertNotaryPubkey>,
expected_notary: Pubkey,
) -> Result<()> {
require_keys_eq!(expected_notary, NOTARY_PUBKEY, TrustChainError::UnauthorizedNotary);
Ok(())
}
pub fn update_integrity(ctx: Context<UpdateIntegrity>, gini_score: u16, hhi_score: u16, status: u8) -> Result<()> {To fully wire this up, add the corresponding accounts struct somewhere in the file (e.g. near your other #[derive(Accounts)] definitions):
#[derive(Accounts)]
pub struct AssertNotaryPubkey {}This empty accounts struct keeps the instruction simple (no runtime dependencies) while allowing off-chain deployment or CI tooling to submit a transaction that verifies the backend’s NOTARY_SECRET-derived public key matches NOTARY_PUBKEY.
Jules Protocol: Authority Rotation & Config
|
Jules Protocol: Fix Notary Sync Account MappingObjectiveUpdate Execution Steps
🚀 Technical Precision: Fixed-Point Math ReminderSince we are using
|
|
@jules I think you're hung up on the account mapping. Please stop the current task and apply these fixes:
Once fixed, push the update to this branch. |
Jules Protocol: Atomic Bootstrap (Task 1)ObjectiveInitialize the GlobalConfig account on-chain. This is the blocker for all other tasks. Execution Steps
NoteDo not attempt to sync user scores yet. Only finish the Config initialization. |
|
@jules I see you're working on the summary. Don't worry about a long explanation. Please just:
Keep it minimal so we can move to the next phase. |
Transition TrustChain to an On-Chain Integrity Protocol.
Changes:
on-chain/trustchain-notary. DefinesUserIntegrityaccount (gini_score,hhi_score,status) andupdate_integrityinstruction. Enforces access control using a hardcoded Notary Public Key.backend/services/notary_sync.tsto simulate fetching verified scores and signing transactions. Implements Ed25519 signature verification to ensure authenticity.NOTARY_SECRETenvironment variable (added.env.example).@coral-xyz/anchorandtweetnacltobackend.PR created automatically by Jules for task 13705485681884725483 started by @Freedomwithin
Summary by Sourcery
Introduce an on-chain TrustChain notary program and a backend notary sync service to record and authorize user integrity scores on Solana.
New Features:
Enhancements:
Build:
Chores: