Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdks/flashtestations-sdk/examples/getFlashtestationTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
1 change: 1 addition & 0 deletions sdks/flashtestations-sdk/examples/verifyBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
31 changes: 31 additions & 0 deletions sdks/flashtestations-sdk/src/rpc/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
42 changes: 37 additions & 5 deletions sdks/flashtestations-sdk/src/rpc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
parseEventLogs,
} from 'viem';

import { getRpcUrl, getChainConfig } from '../config/chains';
import { getRpcUrl, getChainConfig, getContractAddress } from '../config/chains';
import {
BlockParameter,
NetworkError,
Expand All @@ -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
*/
Expand Down Expand Up @@ -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<string[]> {
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
Expand Down Expand Up @@ -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,
};
}

Expand Down
4 changes: 4 additions & 0 deletions sdks/flashtestations-sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand Down Expand Up @@ -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[];
}

/**
Expand Down
3 changes: 1 addition & 2 deletions sdks/flashtestations-sdk/src/verification/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -140,5 +138,6 @@ export async function verifyFlashtestationInBlock(
blockExplorerLink: blockExplorerLink,
builderAddress: flashtestationEvent.caller,
version: flashtestationEvent.version,
sourceLocators: flashtestationEvent.sourceLocators,
};
}
97 changes: 97 additions & 0 deletions sdks/flashtestations-sdk/test/rpc/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);

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

Expand Down
Loading