From a33ea7ae7f19ecba0cd587e14c57417f8a358ada Mon Sep 17 00:00:00 2001 From: Melvillian Date: Thu, 30 Oct 2025 11:58:09 -0400 Subject: [PATCH] feat(flashtestations-sdk): add source locator retrieval from BlockBuilderPolicy contract This commit implements retrieval of source locators (e.g., GitHub URLs) for workload source code by querying the BlockBuilderPolicy contract's getWorkloadMetadata function. The implementation adds a new getSourceLocators method to RpcClient, includes the getWorkloadMetadata ABI definition, and integrates source locators throughout the verification flow. This enables users to trace back to the exact source code repositories and commits used to build TEE workloads, enhancing transparency and reproducibility of flashtestation verifications. --- .../examples/getFlashtestationTx.ts | 1 + .../examples/verifyBlock.ts | 1 + sdks/flashtestations-sdk/src/rpc/abi.ts | 31 ++++++ sdks/flashtestations-sdk/src/rpc/client.ts | 42 +++++++- sdks/flashtestations-sdk/src/types/index.ts | 4 + .../src/verification/service.ts | 3 +- .../test/rpc/client.test.ts | 97 +++++++++++++++++++ 7 files changed, 172 insertions(+), 7 deletions(-) diff --git a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts index 0bee8e7b2..7251b7759 100644 --- a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts +++ b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts @@ -34,6 +34,7 @@ async function main() { console.log(`Version: ${tx.version}`); console.log(`Block Content Hash: ${tx.blockContentHash}`); console.log(`Commit Hash: ${tx.commitHash}`); + console.log(`Source Locators: ${tx.sourceLocators.length > 0 ? tx.sourceLocators.join(', ') : 'None'}`); } else { // This is not a flashtestation transaction console.log('\n✗ This is not a flashtestation transaction.'); diff --git a/sdks/flashtestations-sdk/examples/verifyBlock.ts b/sdks/flashtestations-sdk/examples/verifyBlock.ts index 1faafdf1c..7eb247703 100644 --- a/sdks/flashtestations-sdk/examples/verifyBlock.ts +++ b/sdks/flashtestations-sdk/examples/verifyBlock.ts @@ -47,6 +47,7 @@ async function main() { console.log(`Commit Hash: ${result.commitHash}`); console.log(`Builder Address: ${result.builderAddress}`); console.log(`Version: ${result.version}`); + console.log(`Source Locators: ${result.sourceLocators && result.sourceLocators.length > 0 ? result.sourceLocators.join(', ') : 'None'}`) if (result.blockExplorerLink) { console.log(`Block Explorer: ${result.blockExplorerLink}`); } diff --git a/sdks/flashtestations-sdk/src/rpc/abi.ts b/sdks/flashtestations-sdk/src/rpc/abi.ts index 73033f4ee..996bb1721 100644 --- a/sdks/flashtestations-sdk/src/rpc/abi.ts +++ b/sdks/flashtestations-sdk/src/rpc/abi.ts @@ -33,4 +33,35 @@ export const flashtestationAbi = [ }, ], }, + { + type: 'function', + name: 'getWorkloadMetadata', + inputs: [ + { + name: 'workloadId', + type: 'bytes32', + internalType: 'WorkloadId', + }, + ], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct IBlockBuilderPolicy.WorkloadMetadata', + components: [ + { + name: 'commitHash', + type: 'string', + internalType: 'string', + }, + { + name: 'sourceLocators', + type: 'string[]', + internalType: 'string[]', + }, + ], + }, + ], + stateMutability: 'view', + }, ] as const; diff --git a/sdks/flashtestations-sdk/src/rpc/client.ts b/sdks/flashtestations-sdk/src/rpc/client.ts index d87b4fa7c..93f54a6ac 100644 --- a/sdks/flashtestations-sdk/src/rpc/client.ts +++ b/sdks/flashtestations-sdk/src/rpc/client.ts @@ -9,7 +9,7 @@ import { parseEventLogs, } from 'viem'; -import { getRpcUrl, getChainConfig } from '../config/chains'; +import { getRpcUrl, getChainConfig, getContractAddress } from '../config/chains'; import { BlockParameter, NetworkError, @@ -33,6 +33,11 @@ export interface RpcClientConfig { initialRetryDelay?: number; } +type WorkloadMetadata = { + commitHash: string; + sourceLocators: string[]; +} + /** * Cache of RPC clients keyed by chain ID and RPC URL */ @@ -265,6 +270,33 @@ export class RpcClient { ); } + /** + * Get source locators for a workload ID from the BlockBuilderPolicy contract + * @param workloadId - The workload ID (bytes32 hex string) + * @returns Array of source locator strings + * @throws NetworkError if RPC connection fails + */ + async getSourceLocators(workloadId: `0x${string}`): Promise { + return retry( + async () => { + const contractAddress = getContractAddress(this.config.chainId); + + const result = await this.client.readContract({ + address: contractAddress as `0x${string}`, + abi: flashtestationAbi, + functionName: 'getWorkloadMetadata', + args: [workloadId], + }); + + // result is an object with commitHash and sourceLocators + // We only need the sourceLocators array + return (result as WorkloadMetadata).sourceLocators as string[]; + }, + this.config.maxRetries, + this.config.initialRetryDelay + ); + } + /** * Get a flashtestation event by transaction hash * Checks if the transaction emitted a BlockBuilderProofVerified event @@ -314,16 +346,16 @@ export class RpcClient { commitHash: string; }; - // TODO(melvillian): the event does not include the sourceLocator because of gas optimizations reasons, - // so we need to get the sourceLocator from the block - - + // Fetch source locators from contract + const sourceLocators = await this.getSourceLocators(args.workloadId); + return { caller: args.caller, workloadId: args.workloadId, version: args.version, blockContentHash: args.blockContentHash, commitHash: args.commitHash, + sourceLocators, }; } diff --git a/sdks/flashtestations-sdk/src/types/index.ts b/sdks/flashtestations-sdk/src/types/index.ts index 4f17c8d68..dfa6686fd 100644 --- a/sdks/flashtestations-sdk/src/types/index.ts +++ b/sdks/flashtestations-sdk/src/types/index.ts @@ -14,6 +14,8 @@ export interface VerificationResult { builderAddress?: string; /** Version of the flashtestation protocol, optional */ version: number; + /** Source locators (e.g., GitHub URLs) for the workload source code, optional for backwards compatibility */ + sourceLocators?: string[]; } /** @@ -52,6 +54,8 @@ export interface FlashtestationEvent { blockContentHash: `0x${string}`; /** git commit ID of the code used to reproducibly build the workload (string) */ commitHash: string; + /** Source locators (e.g., GitHub URLs) for the workload source code */ + sourceLocators: string[]; } /** diff --git a/sdks/flashtestations-sdk/src/verification/service.ts b/sdks/flashtestations-sdk/src/verification/service.ts index cbd5c525d..8d61f4973 100644 --- a/sdks/flashtestations-sdk/src/verification/service.ts +++ b/sdks/flashtestations-sdk/src/verification/service.ts @@ -130,8 +130,6 @@ export async function verifyFlashtestationInBlock( blockExplorerLink = `${blockExplorerBaseUrl}/block/${block.number}`; } - // TODO(melvillian): get the sourceLocator from the block - // Block was built by the specified TEE workload return { isBuiltByExpectedTee: true, @@ -140,5 +138,6 @@ export async function verifyFlashtestationInBlock( blockExplorerLink: blockExplorerLink, builderAddress: flashtestationEvent.caller, version: flashtestationEvent.version, + sourceLocators: flashtestationEvent.sourceLocators, }; } diff --git a/sdks/flashtestations-sdk/test/rpc/client.test.ts b/sdks/flashtestations-sdk/test/rpc/client.test.ts index cfd8f0d6c..fd5c7bb00 100644 --- a/sdks/flashtestations-sdk/test/rpc/client.test.ts +++ b/sdks/flashtestations-sdk/test/rpc/client.test.ts @@ -20,10 +20,12 @@ describe('RpcClient', () => { mockGetBlock = jest.fn(); mockGetTransactionReceipt = jest.fn(); + const mockReadContract = jest.fn(); mockClient = { getBlock: mockGetBlock, getTransactionReceipt: mockGetTransactionReceipt, + readContract: mockReadContract, }; // Setup mocks @@ -175,6 +177,89 @@ describe('RpcClient', () => { }); }); + describe('getSourceLocators', () => { + let client: RpcClient; + let mockReadContract: jest.Mock; + + beforeEach(() => { + client = new RpcClient({ chainId: 1301, maxRetries: 0 }); + mockReadContract = mockClient.readContract; + }); + + it('should fetch source locators for a workload ID', async () => { + const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`; + const mockMetadata = { + commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', + sourceLocators: ['https://github.com/example/repo1/c41fa4d500f6fb4e4fe46c23b34b26367e10beb4', 'https://github.com/example/repo2/86ebf9de12466aaae1485eb6fc80ae3c78954edf'] + }; + mockReadContract.mockResolvedValue(mockMetadata); + + const result = await client.getSourceLocators(workloadId); + + expect(mockReadContract).toHaveBeenCalledWith( + expect.objectContaining({ + address: '0x3b03b3caabd49ca12de9eba46a6a2950700b1db4', + functionName: 'getWorkloadMetadata', + args: [workloadId], + }) + ); + expect(result).toEqual(['https://github.com/example/repo1/c41fa4d500f6fb4e4fe46c23b34b26367e10beb4', 'https://github.com/example/repo2/86ebf9de12466aaae1485eb6fc80ae3c78954edf']); + }); + + it('should handle empty source locators', async () => { + const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`; + const mockMetadata = { + commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', + sourceLocators: [] + }; + mockReadContract.mockResolvedValue(mockMetadata); + + const result = await client.getSourceLocators(workloadId); + + expect(result).toEqual([]); + }); + + it('should retry on transient failures', async () => { + const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`; + const mockMetadata = { + commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', + sourceLocators: ['https://github.com/example/repo/86ebf9de12466aaae1485eb6fc80ae3c78954edf'] + }; + + const clientWithRetry = new RpcClient({ + chainId: 1301, + maxRetries: 2, + initialRetryDelay: 10 + }); + const mockReadContractWithRetry = clientWithRetry.getClient().readContract as jest.Mock; + + mockReadContractWithRetry + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce(mockMetadata); + + const result = await clientWithRetry.getSourceLocators(workloadId); + + expect(mockReadContractWithRetry).toHaveBeenCalledTimes(2); + expect(result).toEqual(['https://github.com/example/repo/86ebf9de12466aaae1485eb6fc80ae3c78954edf']); + }); + + it('should throw NetworkError after max retries', async () => { + const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`; + + const clientWithRetry = new RpcClient({ + chainId: 1301, + maxRetries: 1, + initialRetryDelay: 10 + }); + const mockReadContractWithRetry = clientWithRetry.getClient().readContract as jest.Mock; + + mockReadContractWithRetry.mockRejectedValue(new Error('Network error')); + + await expect(clientWithRetry.getSourceLocators(workloadId)).rejects.toThrow(NetworkError); + expect(mockReadContractWithRetry).toHaveBeenCalledTimes(2); + }); + }); + describe('retry logic', () => { it('should retry failed requests with exponential backoff', async () => { const client = new RpcClient({ @@ -284,10 +369,15 @@ describe('RpcClient', () => { commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', }, }; + const mockMetadata = { + commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', + sourceLocators: ['https://github.com/example/repo1/86ebf9de12466aaae1485eb6fc80ae3c78954edf', 'https://github.com/example/repo2/f6cf154d5a26c632548d85998c2a7dab40d8ef02'] + }; mockGetBlock.mockResolvedValue(mockBlock); mockGetTransactionReceipt.mockResolvedValue(mockReceipt); mockParseEventLogs.mockReturnValue([mockLog]); + mockClient.readContract.mockResolvedValue(mockMetadata); const result = await client.getFlashtestationTx(blockNumber); @@ -299,12 +389,19 @@ describe('RpcClient', () => { logs: mockReceipt.logs, }) ); + expect(mockClient.readContract).toHaveBeenCalledWith( + expect.objectContaining({ + functionName: 'getWorkloadMetadata', + args: ['0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f'], + }) + ); expect(result).toEqual({ caller: '0xcaBBa9e7f4b3A885C5aa069f88469ac711Dd4aCC', workloadId: '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f', version: 1, blockContentHash: '0x846604baa7db2297b9c4058106cc5869bcdbb753760981dbcd6d345d3d5f3e0f', commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844', + sourceLocators: ['https://github.com/example/repo1/86ebf9de12466aaae1485eb6fc80ae3c78954edf', 'https://github.com/example/repo2/f6cf154d5a26c632548d85998c2a7dab40d8ef02'], }); });