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'], }); });