Skip to content

Commit

Permalink
Add verified builds support (#371)
Browse files Browse the repository at this point in the history
This PR adds a row to account headers for Program Accounts like so:
![Screenshot 2024-09-03 at 5 48
45 PM](https://github.com/user-attachments/assets/d135adef-c055-4e0b-9e9e-614de6a5b4b4)

---------

Co-authored-by: Noah Gundotra <[email protected]>
  • Loading branch information
ngundotra and Noah Gundotra authored Sep 10, 2024
1 parent 8229ee3 commit 8e2c735
Show file tree
Hide file tree
Showing 9 changed files with 6,341 additions and 4,612 deletions.
8 changes: 7 additions & 1 deletion app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = {
slug: 'security',
title: 'Security',
},
{
path: 'verified-build',
slug: 'verified-build',
title: 'Verified Build',
},
],
'nftoken:collection': [
{
Expand Down Expand Up @@ -555,7 +560,8 @@ export type MoreTabs =
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree'
| 'compression';
| 'compression'
| 'verified-build';

function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) {
return (
Expand Down
27 changes: 27 additions & 0 deletions app/address/[address]/verified-build/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import React from 'react';

import { VerifiedBuildCard } from '@/app/components/account/VerifiedBuildCard';

type Props = Readonly<{
params: {
address: string;
};
}>;

function VerifiedBuildCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') {
return onNotFound();
}
return <VerifiedBuildCard data={parsedData} pubkey={account.pubkey} />;
}

export default function SecurityPageClient({ params: { address } }: Props) {
return <ParsedAccountRenderer address={address} renderComponent={VerifiedBuildCardRenderer} />;
}
21 changes: 21 additions & 0 deletions app/address/[address]/verified-build/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import VerifiedBuildClient from './page-client';

export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Contents of the verified build info for the program with address ${props.params.address} on Solana`,
title: `Verified Build | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

type Props = Readonly<{
params: {
address: string;
};
}>;

export default function VerifiedBuildPage(props: Props) {
return <VerifiedBuildClient {...props} />;
}
43 changes: 23 additions & 20 deletions app/components/account/UpgradeableLoaderAccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import Link from 'next/link';
import React from 'react';
import { ExternalLink, RefreshCw } from 'react-feather';

import { VerifiedProgramBadge } from '../common/VerifiedProgramBadge';

export function UpgradeableLoaderAccountSection({
account,
parsedData,
Expand Down Expand Up @@ -110,26 +112,12 @@ export function UpgradeableProgramSection({
<td className="text-lg-end">{programData.authority !== null ? 'Yes' : 'No'}</td>
</tr>
<tr>
{/* Anchor Program Registry is no longer maintained, so the verified label has been removed */}
{/*
<td>
<LastVerifiedBuildLabel />
</td>
<td className="text-lg-end">
{loading ? (
<CheckingBadge />
) : (
<>
{verifiableBuilds.map((b, i) => (
<VerifiedBadge
key={i}
verifiableBuild={b}
deploySlot={programData.slot}
/>
))}
</>
)}
</td> */}
<td>
<VerifiedLabel />
</td>
<td className="text-lg-end">
<VerifiedProgramBadge programData={programData} pubkey={account.pubkey} />
</td>
</tr>
<tr>
<td>
Expand Down Expand Up @@ -171,6 +159,21 @@ function SecurityLabel() {
);
}

function VerifiedLabel() {
return (
<InfoTooltip text="Verified builds allow users can ensure that the hash of the on-chain program matches the hash of the program of the given codebase (registry hosted by osec.io).">
<Link
rel="noopener noreferrer"
target="_blank"
href="https://github.com/Ellipsis-Labs/solana-verifiable-build"
>
<span className="security-txt-link-color-hack-reee">Verified Build</span>
<ExternalLink className="align-text-top ms-2" size={13} />
</Link>
</InfoTooltip>
);
}

export function UpgradeableProgramDataSection({
account,
programData,
Expand Down
137 changes: 137 additions & 0 deletions app/components/account/VerifiedBuildCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ErrorCard } from '@components/common/ErrorCard';
import { TableCardBody } from '@components/common/TableCardBody';
import { UpgradeableLoaderAccountData } from '@providers/accounts';
import { PublicKey } from '@solana/web3.js';
import { ExternalLink } from 'react-feather';

import { OsecRegistryInfo, useVerifiedProgramRegistry } from '@/app/utils/verified-builds';

import { LoadingCard } from '../common/LoadingCard';

export function VerifiedBuildCard({ data, pubkey }: { data: UpgradeableLoaderAccountData; pubkey: PublicKey }) {
const { data: registryInfo, isLoading } = useVerifiedProgramRegistry({
options: { suspense: true },
programId: pubkey,
});
if (!data.programData) {
return <ErrorCard text="Account has no data" />;
}

if (isLoading) {
return <LoadingCard message="Fetching last verified build hash" />;
}

if (!registryInfo) {
return <ErrorCard text="No verified build found" />;
}

return (
<div className="card security-txt">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">Verified Build</h3>
<small>Information provided by osec.io</small>
</div>
<TableCardBody>
{ROWS.filter(x => x.key in registryInfo).map((x, idx) => {
return (
<tr key={idx}>
<td className="w-100">{x.display}</td>
<RenderEntry value={registryInfo[x.key]} type={x.type} />
</tr>
);
})}
</TableCardBody>
</div>
);
}

enum DisplayType {
Boolean,
String,
URL,
Date,
}

type TableRow = {
display: string;
key: keyof OsecRegistryInfo;
type: DisplayType;
};

const ROWS: TableRow[] = [
{
display: 'Verified',
key: 'is_verified',
type: DisplayType.Boolean,
},
{
display: 'Message',
key: 'message',
type: DisplayType.String,
},
{
display: 'On Chain Hash',
key: 'on_chain_hash',
type: DisplayType.String,
},
{
display: 'Executable Hash',
key: 'executable_hash',
type: DisplayType.String,
},
{
display: 'Last Verified At',
key: 'last_verified_at',
type: DisplayType.Date,
},
{
display: 'Repository URL',
key: 'repo_url',
type: DisplayType.URL,
},
];

function RenderEntry({ value, type }: { value: OsecRegistryInfo[keyof OsecRegistryInfo]; type: DisplayType }) {
switch (type) {
case DisplayType.Boolean:
return (
<td className={'text-lg-end font-monospace'}>
<span className={`badge bg-${value ? 'success' : 'warning'}-soft`}>{new String(value)}</span>
</td>
);
case DisplayType.String:
return <td className="text-lg-end font-monospace" style={{whiteSpace: 'pre'}}>{value && (value as string).length > 1 ? value : '-'}</td>;
case DisplayType.URL:
if (isValidLink(value as string)) {
return (
<td className="text-lg-end">
<span className="font-monospace">
<a rel="noopener noreferrer" target="_blank" href={value as string}>
{value}
<ExternalLink className="align-text-top ms-2" size={13} />
</a>
</span>
</td>
);
}
return (
<td className="text-lg-end font-monospace">
{value && (value as string).length > 1 ? (value as string).trim() : '-'}
</td>
);
case DisplayType.Date:
return <td className="text-lg-end font-monospace">{value && (value as string).length > 1 ? new Date(value as string).toUTCString() : '-'}</td>;
default:
break;
}
return <></>;
}

function isValidLink(value: string) {
try {
const url = new URL(value);
return ['http:', 'https:'].includes(url.protocol);
} catch (err) {
return false;
}
}
43 changes: 43 additions & 0 deletions app/components/common/VerifiedProgramBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { PublicKey } from '@solana/web3.js';
import Link from 'next/link';

import { useClusterPath } from '@/app/utils/url';
import { hashProgramData, useVerifiedProgramRegistry } from '@/app/utils/verified-builds';
import { ProgramDataAccountInfo } from '@/app/validators/accounts/upgradeable-program';

export function VerifiedProgramBadge({
programData,
pubkey,
}: {
programData: ProgramDataAccountInfo;
pubkey: PublicKey;
}) {
const { isLoading, data: registryInfo } = useVerifiedProgramRegistry({ programId: pubkey });
const verifiedBuildTabPath = useClusterPath({ pathname: `/address/${pubkey.toBase58()}/verified-build` });

const hash = hashProgramData(programData);

if (isLoading) {
return (
<h3 className="mb-0">
<span className="badge">Loading...</span>
</h3>
);
} else if (registryInfo && hash === registryInfo['on_chain_hash'] && registryInfo['is_verified']) {
return (
<h3 className="mb-0">
<Link className="badge bg-success-soft rank" href={verifiedBuildTabPath}>
Program Source Program Verified
</Link>
</h3>
);
} else {
const message =
!registryInfo || !registryInfo['repo_url'] ? 'Source Code Not Provided' : 'Program Not Verified';
return (
<h3 className="mb-0">
<span className="badge bg-warning-soft rank">{message}</span>
</h3>
);
}
}
49 changes: 49 additions & 0 deletions app/utils/verified-builds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { sha256 } from '@noble/hashes/sha256';
import { PublicKey } from '@solana/web3.js';
import useSWRImmutable from 'swr/immutable';

import { ProgramDataAccountInfo } from '../validators/accounts/upgradeable-program';

const OSEC_REGISTRY_URL = 'https://verify.osec.io';

export type OsecRegistryInfo = {
is_verified: boolean;
message: string;
on_chain_hash: string;
executable_hash: string;
last_verified_at: string | null;
repo_url: string;
};

export type CheckedOsecRegistryInfo = {
explorer_hash: string;
};

export function useVerifiedProgramRegistry({
programId,
options,
}: {
programId: PublicKey;
options?: { suspense: boolean };
}) {
const { data, error, isLoading } = useSWRImmutable(
`${programId.toBase58()}`,
async (programId: string) => {
return fetch(`${OSEC_REGISTRY_URL}/status/${programId}`).then(response => response.json());
},
{ suspense: options?.suspense }
);
return { data: error ? null : (data as OsecRegistryInfo), isLoading };
}

export function hashProgramData(programData: ProgramDataAccountInfo): string {
const buffer = Buffer.from(programData.data[0], 'base64');
// Truncate null bytes at the end of the buffer
let truncatedBytes = 0;
while (buffer[buffer.length - 1 - truncatedBytes] === 0) {
truncatedBytes++;
}
// Hash the binary
const c = Buffer.from(buffer.slice(0, buffer.length - truncatedBytes));
return Buffer.from(sha256(c)).toString('hex');
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@metamask/jazzicon": "^2.0.0",
"@metaplex-foundation/mpl-token-metadata": "^1.1.0",
"@metaplex/js": "^4.12.0",
"@noble/hashes": "^1.5.0",
"@onsol/tldparser": "^0.6.5",
"@project-serum/anchor": "^0.23.0",
"@project-serum/serum": "^0.13.61",
Expand Down
Loading

0 comments on commit 8e2c735

Please sign in to comment.