Skip to content

Commit b94a33d

Browse files
committed
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.
1 parent 99210c2 commit b94a33d

File tree

7 files changed

+172
-7
lines changed

7 files changed

+172
-7
lines changed

sdks/flashtestations-sdk/examples/getFlashtestationTx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ async function main() {
3434
console.log(`Version: ${tx.version}`);
3535
console.log(`Block Content Hash: ${tx.blockContentHash}`);
3636
console.log(`Commit Hash: ${tx.commitHash}`);
37+
console.log(`Source Locators: ${tx.sourceLocators.length > 0 ? tx.sourceLocators.join(', ') : 'None'}`);
3738
} else {
3839
// This is not a flashtestation transaction
3940
console.log('\n✗ This is not a flashtestation transaction.');

sdks/flashtestations-sdk/examples/verifyBlock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async function main() {
4747
console.log(`Commit Hash: ${result.commitHash}`);
4848
console.log(`Builder Address: ${result.builderAddress}`);
4949
console.log(`Version: ${result.version}`);
50+
console.log(`Source Locators: ${result.sourceLocators && result.sourceLocators.length > 0 ? result.sourceLocators.join(', ') : 'None'}`)
5051
if (result.blockExplorerLink) {
5152
console.log(`Block Explorer: ${result.blockExplorerLink}`);
5253
}

sdks/flashtestations-sdk/src/rpc/abi.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,35 @@ export const flashtestationAbi = [
3333
},
3434
],
3535
},
36+
{
37+
type: 'function',
38+
name: 'getWorkloadMetadata',
39+
inputs: [
40+
{
41+
name: 'workloadId',
42+
type: 'bytes32',
43+
internalType: 'WorkloadId',
44+
},
45+
],
46+
outputs: [
47+
{
48+
name: '',
49+
type: 'tuple',
50+
internalType: 'struct IBlockBuilderPolicy.WorkloadMetadata',
51+
components: [
52+
{
53+
name: 'commitHash',
54+
type: 'string',
55+
internalType: 'string',
56+
},
57+
{
58+
name: 'sourceLocators',
59+
type: 'string[]',
60+
internalType: 'string[]',
61+
},
62+
],
63+
},
64+
],
65+
stateMutability: 'view',
66+
},
3667
] as const;

sdks/flashtestations-sdk/src/rpc/client.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
parseEventLogs,
1010
} from 'viem';
1111

12-
import { getRpcUrl, getChainConfig } from '../config/chains';
12+
import { getRpcUrl, getChainConfig, getContractAddress } from '../config/chains';
1313
import {
1414
BlockParameter,
1515
NetworkError,
@@ -33,6 +33,11 @@ export interface RpcClientConfig {
3333
initialRetryDelay?: number;
3434
}
3535

36+
type WorkloadMetadata = {
37+
commitHash: string;
38+
sourceLocators: string[];
39+
}
40+
3641
/**
3742
* Cache of RPC clients keyed by chain ID and RPC URL
3843
*/
@@ -265,6 +270,33 @@ export class RpcClient {
265270
);
266271
}
267272

273+
/**
274+
* Get source locators for a workload ID from the BlockBuilderPolicy contract
275+
* @param workloadId - The workload ID (bytes32 hex string)
276+
* @returns Array of source locator strings
277+
* @throws NetworkError if RPC connection fails
278+
*/
279+
async getSourceLocators(workloadId: `0x${string}`): Promise<string[]> {
280+
return retry(
281+
async () => {
282+
const contractAddress = getContractAddress(this.config.chainId);
283+
284+
const result = await this.client.readContract({
285+
address: contractAddress as `0x${string}`,
286+
abi: flashtestationAbi,
287+
functionName: 'getWorkloadMetadata',
288+
args: [workloadId],
289+
});
290+
291+
// result is an object with commitHash and sourceLocators
292+
// We only need the sourceLocators array
293+
return (result as WorkloadMetadata).sourceLocators as string[];
294+
},
295+
this.config.maxRetries,
296+
this.config.initialRetryDelay
297+
);
298+
}
299+
268300
/**
269301
* Get a flashtestation event by transaction hash
270302
* Checks if the transaction emitted a BlockBuilderProofVerified event
@@ -314,16 +346,16 @@ export class RpcClient {
314346
commitHash: string;
315347
};
316348

317-
// TODO(melvillian): the event does not include the sourceLocator because of gas optimizations reasons,
318-
// so we need to get the sourceLocator from the block
319-
320-
349+
// Fetch source locators from contract
350+
const sourceLocators = await this.getSourceLocators(args.workloadId);
351+
321352
return {
322353
caller: args.caller,
323354
workloadId: args.workloadId,
324355
version: args.version,
325356
blockContentHash: args.blockContentHash,
326357
commitHash: args.commitHash,
358+
sourceLocators,
327359
};
328360
}
329361

sdks/flashtestations-sdk/src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface VerificationResult {
1414
builderAddress?: string;
1515
/** Version of the flashtestation protocol, optional */
1616
version: number;
17+
/** Source locators (e.g., GitHub URLs) for the workload source code, optional for backwards compatibility */
18+
sourceLocators?: string[];
1719
}
1820

1921
/**
@@ -52,6 +54,8 @@ export interface FlashtestationEvent {
5254
blockContentHash: `0x${string}`;
5355
/** git commit ID of the code used to reproducibly build the workload (string) */
5456
commitHash: string;
57+
/** Source locators (e.g., GitHub URLs) for the workload source code */
58+
sourceLocators: string[];
5559
}
5660

5761
/**

sdks/flashtestations-sdk/src/verification/service.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ export async function verifyFlashtestationInBlock(
130130
blockExplorerLink = `${blockExplorerBaseUrl}/block/${block.number}`;
131131
}
132132

133-
// TODO(melvillian): get the sourceLocator from the block
134-
135133
// Block was built by the specified TEE workload
136134
return {
137135
isBuiltByExpectedTee: true,
@@ -140,5 +138,6 @@ export async function verifyFlashtestationInBlock(
140138
blockExplorerLink: blockExplorerLink,
141139
builderAddress: flashtestationEvent.caller,
142140
version: flashtestationEvent.version,
141+
sourceLocators: flashtestationEvent.sourceLocators,
143142
};
144143
}

sdks/flashtestations-sdk/test/rpc/client.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ describe('RpcClient', () => {
2020

2121
mockGetBlock = jest.fn();
2222
mockGetTransactionReceipt = jest.fn();
23+
const mockReadContract = jest.fn();
2324

2425
mockClient = {
2526
getBlock: mockGetBlock,
2627
getTransactionReceipt: mockGetTransactionReceipt,
28+
readContract: mockReadContract,
2729
};
2830

2931
// Setup mocks
@@ -175,6 +177,89 @@ describe('RpcClient', () => {
175177
});
176178
});
177179

180+
describe('getSourceLocators', () => {
181+
let client: RpcClient;
182+
let mockReadContract: jest.Mock;
183+
184+
beforeEach(() => {
185+
client = new RpcClient({ chainId: 1301, maxRetries: 0 });
186+
mockReadContract = mockClient.readContract;
187+
});
188+
189+
it('should fetch source locators for a workload ID', async () => {
190+
const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`;
191+
const mockMetadata = {
192+
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
193+
sourceLocators: ['https://github.com/example/repo1/c41fa4d500f6fb4e4fe46c23b34b26367e10beb4', 'https://github.com/example/repo2/86ebf9de12466aaae1485eb6fc80ae3c78954edf']
194+
};
195+
mockReadContract.mockResolvedValue(mockMetadata);
196+
197+
const result = await client.getSourceLocators(workloadId);
198+
199+
expect(mockReadContract).toHaveBeenCalledWith(
200+
expect.objectContaining({
201+
address: '0x3b03b3caabd49ca12de9eba46a6a2950700b1db4',
202+
functionName: 'getWorkloadMetadata',
203+
args: [workloadId],
204+
})
205+
);
206+
expect(result).toEqual(['https://github.com/example/repo1/c41fa4d500f6fb4e4fe46c23b34b26367e10beb4', 'https://github.com/example/repo2/86ebf9de12466aaae1485eb6fc80ae3c78954edf']);
207+
});
208+
209+
it('should handle empty source locators', async () => {
210+
const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`;
211+
const mockMetadata = {
212+
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
213+
sourceLocators: []
214+
};
215+
mockReadContract.mockResolvedValue(mockMetadata);
216+
217+
const result = await client.getSourceLocators(workloadId);
218+
219+
expect(result).toEqual([]);
220+
});
221+
222+
it('should retry on transient failures', async () => {
223+
const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`;
224+
const mockMetadata = {
225+
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
226+
sourceLocators: ['https://github.com/example/repo/86ebf9de12466aaae1485eb6fc80ae3c78954edf']
227+
};
228+
229+
const clientWithRetry = new RpcClient({
230+
chainId: 1301,
231+
maxRetries: 2,
232+
initialRetryDelay: 10
233+
});
234+
const mockReadContractWithRetry = clientWithRetry.getClient().readContract as jest.Mock;
235+
236+
mockReadContractWithRetry
237+
.mockRejectedValueOnce(new Error('Network error'))
238+
.mockResolvedValueOnce(mockMetadata);
239+
240+
const result = await clientWithRetry.getSourceLocators(workloadId);
241+
242+
expect(mockReadContractWithRetry).toHaveBeenCalledTimes(2);
243+
expect(result).toEqual(['https://github.com/example/repo/86ebf9de12466aaae1485eb6fc80ae3c78954edf']);
244+
});
245+
246+
it('should throw NetworkError after max retries', async () => {
247+
const workloadId = '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f' as `0x${string}`;
248+
249+
const clientWithRetry = new RpcClient({
250+
chainId: 1301,
251+
maxRetries: 1,
252+
initialRetryDelay: 10
253+
});
254+
const mockReadContractWithRetry = clientWithRetry.getClient().readContract as jest.Mock;
255+
256+
mockReadContractWithRetry.mockRejectedValue(new Error('Network error'));
257+
258+
await expect(clientWithRetry.getSourceLocators(workloadId)).rejects.toThrow(NetworkError);
259+
expect(mockReadContractWithRetry).toHaveBeenCalledTimes(2);
260+
});
261+
});
262+
178263
describe('retry logic', () => {
179264
it('should retry failed requests with exponential backoff', async () => {
180265
const client = new RpcClient({
@@ -284,10 +369,15 @@ describe('RpcClient', () => {
284369
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
285370
},
286371
};
372+
const mockMetadata = {
373+
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
374+
sourceLocators: ['https://github.com/example/repo1/86ebf9de12466aaae1485eb6fc80ae3c78954edf', 'https://github.com/example/repo2/f6cf154d5a26c632548d85998c2a7dab40d8ef02']
375+
};
287376

288377
mockGetBlock.mockResolvedValue(mockBlock);
289378
mockGetTransactionReceipt.mockResolvedValue(mockReceipt);
290379
mockParseEventLogs.mockReturnValue([mockLog]);
380+
mockClient.readContract.mockResolvedValue(mockMetadata);
291381

292382
const result = await client.getFlashtestationTx(blockNumber);
293383

@@ -299,12 +389,19 @@ describe('RpcClient', () => {
299389
logs: mockReceipt.logs,
300390
})
301391
);
392+
expect(mockClient.readContract).toHaveBeenCalledWith(
393+
expect.objectContaining({
394+
functionName: 'getWorkloadMetadata',
395+
args: ['0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f'],
396+
})
397+
);
302398
expect(result).toEqual({
303399
caller: '0xcaBBa9e7f4b3A885C5aa069f88469ac711Dd4aCC',
304400
workloadId: '0x71d62ba17902d590dad932310a7ec12feffa25454d7009c2084aa6f4c488953f',
305401
version: 1,
306402
blockContentHash: '0x846604baa7db2297b9c4058106cc5869bcdbb753760981dbcd6d345d3d5f3e0f',
307403
commitHash: '490fb2be109f0c2626c347bb3e43e97826c8f844',
404+
sourceLocators: ['https://github.com/example/repo1/86ebf9de12466aaae1485eb6fc80ae3c78954edf', 'https://github.com/example/repo2/f6cf154d5a26c632548d85998c2a7dab40d8ef02'],
308405
});
309406
});
310407

0 commit comments

Comments
 (0)