Skip to content

Commit cc21e5a

Browse files
authored
feat(flashtestations-sdk): add source locator retrieval from BlockBui… (#424)
1 parent 2481f90 commit cc21e5a

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)