From c10c8ff890ceeadaa090398a53bd37afab948fda Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 30 Oct 2024 19:57:28 +0100 Subject: [PATCH] Add verify command to verified build tab (#396) - Deriving the PDA from the upgrade authority and the program id, then getting the parameter from the PDA and composing it into a solana-verify comand Like this people can easily copy the command from the explorer and verify for themselfs. Related Issue: https://github.com/solana-labs/explorer/issues/393 Looks like this: image And like this when there is no Verify PDA: image --------- Co-authored-by: Noah Gundotra --- app/components/account/VerifiedBuildCard.tsx | 53 ++++++++- .../common/VerifiedProgramBadge.tsx | 2 +- app/utils/verified-builds.tsx | 102 ++++++++++++++++-- 3 files changed, 146 insertions(+), 11 deletions(-) diff --git a/app/components/account/VerifiedBuildCard.tsx b/app/components/account/VerifiedBuildCard.tsx index 1928d518..a752c251 100644 --- a/app/components/account/VerifiedBuildCard.tsx +++ b/app/components/account/VerifiedBuildCard.tsx @@ -6,11 +6,13 @@ import { ExternalLink } from 'react-feather'; import { OsecRegistryInfo, useVerifiedProgramRegistry } from '@/app/utils/verified-builds'; +import { Copyable } from '../common/Copyable'; import { LoadingCard } from '../common/LoadingCard'; export function VerifiedBuildCard({ data, pubkey }: { data: UpgradeableLoaderAccountData; pubkey: PublicKey }) { const { data: registryInfo, isLoading } = useVerifiedProgramRegistry({ options: { suspense: true }, + programAuthority: data.programData?.authority ? new PublicKey(data.programData.authority) : null, programId: pubkey, }); if (!data.programData) { @@ -31,6 +33,18 @@ export function VerifiedBuildCard({ data, pubkey }: { data: UpgradeableLoaderAcc

Verified Build

Information provided by osec.io +
+ Verified builds indicate that the onchain build was built from the source code that is publicly + available, but this does not imply a security audit. For more details, refer to the{' '} + + Verified Builds Docs + + . +
{ROWS.filter(x => x.key in registryInfo).map((x, idx) => { return ( @@ -50,6 +64,7 @@ enum DisplayType { String, URL, Date, + LongString, } type TableRow = { @@ -84,6 +99,11 @@ const ROWS: TableRow[] = [ key: 'last_verified_at', type: DisplayType.Date, }, + { + display: 'Verify Command', + key: 'verify_command', + type: DisplayType.LongString, + }, { display: 'Repository URL', key: 'repo_url', @@ -100,7 +120,32 @@ function RenderEntry({ value, type }: { value: OsecRegistryInfo[keyof OsecRegist ); case DisplayType.String: - return {value && (value as string).length > 1 ? value : '-'}; + return ( + + {value && (value as string).length > 1 ? value : '-'} + + ); + case DisplayType.LongString: + return ( + + {value && (value as string).length > 1 ? ( + <> + + {value} + + ) : ( + '-' + )} + + ); case DisplayType.URL: if (isValidLink(value as string)) { return ( @@ -120,7 +165,11 @@ function RenderEntry({ value, type }: { value: OsecRegistryInfo[keyof OsecRegist ); case DisplayType.Date: - return {value && (value as string).length > 1 ? new Date(value as string).toUTCString() : '-'}; + return ( + + {value && (value as string).length > 1 ? new Date(value as string).toUTCString() : '-'} + + ); default: break; } diff --git a/app/components/common/VerifiedProgramBadge.tsx b/app/components/common/VerifiedProgramBadge.tsx index f683bfae..89ab2799 100644 --- a/app/components/common/VerifiedProgramBadge.tsx +++ b/app/components/common/VerifiedProgramBadge.tsx @@ -12,7 +12,7 @@ export function VerifiedProgramBadge({ programData: ProgramDataAccountInfo; pubkey: PublicKey; }) { - const { isLoading, data: registryInfo } = useVerifiedProgramRegistry({ programId: pubkey }); + const { isLoading, data: registryInfo } = useVerifiedProgramRegistry({ programAuthority: programData.authority ? new PublicKey(programData.authority) : null, programId: pubkey }); const verifiedBuildTabPath = useClusterPath({ pathname: `/address/${pubkey.toBase58()}/verified-build` }); const hash = hashProgramData(programData); diff --git a/app/utils/verified-builds.tsx b/app/utils/verified-builds.tsx index 1eb84958..b8ba4ada 100644 --- a/app/utils/verified-builds.tsx +++ b/app/utils/verified-builds.tsx @@ -1,10 +1,14 @@ import { sha256 } from '@noble/hashes/sha256'; -import { PublicKey } from '@solana/web3.js'; +import { Connection, PublicKey } from '@solana/web3.js'; import useSWRImmutable from 'swr/immutable'; +import { useAnchorProgram } from '../providers/anchor'; +import { useCluster } from '../providers/cluster'; import { ProgramDataAccountInfo } from '../validators/accounts/upgradeable-program'; +import { Cluster } from './cluster'; const OSEC_REGISTRY_URL = 'https://verify.osec.io'; +const VERIFY_PROGRAM_ID = 'verifycLy8mB96wd9wqq3WDXQwM4oU6r42Th37Db9fC'; export type OsecRegistryInfo = { is_verified: boolean; @@ -13,29 +17,111 @@ export type OsecRegistryInfo = { executable_hash: string; last_verified_at: string | null; repo_url: string; -}; - -export type CheckedOsecRegistryInfo = { - explorer_hash: string; + verify_command: string; }; export function useVerifiedProgramRegistry({ programId, + programAuthority, options, }: { programId: PublicKey; + programAuthority: PublicKey | null; options?: { suspense: boolean }; }) { - const { data, error, isLoading } = useSWRImmutable( + const { url: clusterUrl, cluster: cluster } = useCluster(); + const connection = new Connection(clusterUrl); + + const { + data: registryData, + error: registryError, + isLoading: isRegistryLoading, + } = useSWRImmutable( `${programId.toBase58()}`, async (programId: string) => { - return fetch(`${OSEC_REGISTRY_URL}/status/${programId}`).then(response => response.json()); + const response = await fetch(`${OSEC_REGISTRY_URL}/status/${programId}`); + return response.json(); + }, + { suspense: options?.suspense } + ); + + const { program: accountAnchorProgram } = useAnchorProgram(VERIFY_PROGRAM_ID, connection.rpcEndpoint); + + // Fetch the PDA derived from the program upgrade authority + // TODO: Add getting verifier pubkey from the security.txt as second option once implemented + const { + data: pdaData, + error: pdaError, + isLoading: isPdaLoading, + } = useSWRImmutable( + programAuthority && accountAnchorProgram ? `pda-${programId.toBase58()}` : null, + async () => { + const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from('otter_verify'), programAuthority!.toBuffer(), programId.toBuffer()], + new PublicKey(VERIFY_PROGRAM_ID) + ); + const pdaAccountInfo = await connection.getAccountInfo(pda); + if (!pdaAccountInfo || !pdaAccountInfo.data) { + console.log('PDA account info not found'); + return null; + } + return accountAnchorProgram?.coder.accounts.decode('buildParams', pdaAccountInfo.data); }, { suspense: options?.suspense } ); - return { data: error ? null : (data as OsecRegistryInfo), isLoading }; + + const isLoading = isRegistryLoading || isPdaLoading; + + if (registryError || pdaError) { + return { data: null, error: registryError || pdaError, isLoading }; + } + + // Create command from the args of the verify PDA + if (registryData && pdaData && !isLoading) { + const verifiedData = registryData as OsecRegistryInfo; + verifiedData.verify_command = `solana-verify verify-from-repo -um --program-id ${programId.toBase58()} ${ + pdaData.gitUrl + } --commit-hash ${pdaData.commit}`; + + // Add additional args if available, for example mount-path and --library-name + if (pdaData.args && pdaData.args.length > 0) { + const filteredArgs = []; + + for (let i = 0; i < pdaData.args.length; i++) { + const arg = pdaData.args[i]; + + if (arg === '-b' || arg === '--base-image') { + i++; // Also skip the parameter + continue; + } + filteredArgs.push(arg); + } + + if (filteredArgs.length > 0) { + const argsString = filteredArgs.join(' '); + verifiedData.verify_command += ` ${argsString}`; + } + } + + return { data: verifiedData, isLoading }; + } + if (registryData && pdaData == null && !isLoading) { + const verifiedData = registryData as OsecRegistryInfo; + + verifiedData.verify_command = isMainnet(cluster) + ? 'Program does not have a verify PDA uploaded.' + : 'Verify command only available on mainnet.'; + return { data: verifiedData, isLoading }; + } + + return { data: null, isLoading }; +} + +function isMainnet(currentCluster: Cluster): boolean { + return currentCluster == Cluster.MainnetBeta; } +// Helper function to hash program data export function hashProgramData(programData: ProgramDataAccountInfo): string { const buffer = Buffer.from(programData.data[0], 'base64'); // Truncate null bytes at the end of the buffer