diff --git a/sdks/flashtestations-sdk/.gitignore b/sdks/flashtestations-sdk/.gitignore index 4c9d7c35a..89c77d417 100644 --- a/sdks/flashtestations-sdk/.gitignore +++ b/sdks/flashtestations-sdk/.gitignore @@ -2,3 +2,5 @@ .DS_Store node_modules dist + +.claude-output/ diff --git a/sdks/flashtestations-sdk/README.md b/sdks/flashtestations-sdk/README.md index 93eb55df4..9eb979b05 100644 --- a/sdks/flashtestations-sdk/README.md +++ b/sdks/flashtestations-sdk/README.md @@ -1,103 +1,337 @@ -# TSDX User Guide +# Flashtestations SDK -Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. +## Overview -> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. +Flashtestations are cryptographic proofs that blockchain blocks were built by Trusted Execution Environments (TEEs) running a specific version of [Flashbot's op-rbuilder](https://github.com/flashbots/op-rbuilder/tree/main), which is the TEE-based builder used to build blocks on Unichain. This SDK allows you to verify whether blocks on Unichain networks were built by the expected versions of op-rbuilder running in a TEE. Unlike on other blockchains where you have no guarantee and thus must trust the block builder to build blocks [fairly](https://www.paradigm.xyz/2024/06/priority-is-all-you-need), with flashtestations you can cryptographically verify that a Unichain block has been built with a particular version of op-rbuilder. -> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) +The TEE devices that run Unichain's builder software provide hardware-enforced isolation and attestation, enabling transparent and verifiable block building. Each TEE workload (i.e. a specific version of op-rbuilder running in a TEE) is uniquely identified by measurement registers that cryptographically commit to the exact software running inside the TEE. When op-rbuilder builds a block on Unichain, it emits a "flashtestation" transaction as the last transaction in the block that proves which workload built that block. -## Commands +This SDK simplifies the verification process by providing a single function to check if a block contains a valid flashtestation matching your expected workload ID. For more background on flashtestations and TEE-based block building, see the [flashtestations spec](https://github.com/flashbots/rollup-boost/blob/main/specs/flashtestations.md) and the [flashtestations smart contracts](https://github.com/flashbots/flashtestations). -TSDX scaffolds your new library inside `/src`. +## Getting Started -To run TSDX, use: +### Installation ```bash -npm start # or yarn start +npm install flashtestations-sdk +# or +yarn add flashtestations-sdk ``` -This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. +### Quick Start + +```typescript +import { verifyFlashtestationInBlock } from 'flashtestations-sdk'; + +async function main() { + // Verify if the latest block on Unichain Mainnet was built by a specific TEE workload + const result = await verifyFlashtestationInBlock( + '0x306ab4fe782dde50a97584b6d4cad9375f7b5d02199c4c78821ad6622670c6b7', // Your expected workload ID + 'latest', // Block to verify (can be 'latest', 'pending', 'safe', 'finalized', number, or hash) + { chainId: 130 } // Unichain Mainnet + ); + + if (result.isBuiltByExpectedTee) { + console.log('✓ Block was built by the expected TEE workload!'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + } else { + console.log('\n✗ Block was NOT built by the specified TEE workload\n'); + + if (result.workloadMetadata) { + console.log('Block was built by a different TEE workload:'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log( + `Source Locators: ${ + result.workloadMetadata.sourceLocators.length > 0 + ? result.workloadMetadata.sourceLocators.join(', ') + : 'None' + }` + ); + } else { + console.log('The block does not contain a flashtestation transaction'); + } + } +} + +// run the quick start +main(); +``` + +## Supported Chains + +| Chain | Chain ID | Status | RPC Configuration | +| ---------------- | -------- | ---------- | ----------------- | +| Unichain Mainnet | 130 | Production | Auto-configured | +| Unichain Sepolia | 1301 | Testnet | Auto-configured | + +## How Do I Acquire a Particular op-rbuilder's Workload ID? + +The Flashtestations protocol exists to let you cryptographically verify that a particular version of op-rbuilder is in fact building the latest block's on Unichain. To cryptographically identify these op-rbuilder versions across all of the various components (the TEE, the smart contracts, and SDK) we use a 32-byte workload ID, which is a [hash of the important components of the TEE attestation](https://github.com/flashbots/flashtestations/blob/7cc7f68492fe672a823dd2dead649793aac1f216/src/BlockBuilderPolicy.sol#L224). But this workload ID tells us nothing about what op-rbuilder source code the builder operators used to build the final Linux OS image that runs on the TEE. We need a trustless (i.e. locally verifiable) method for calculating the workload ID, given a version of op-rbuilder. + +With a small caveat we'll explain shortly, that process is what the [flashbots-images](https://github.com/flashbots/flashbots-images/commits/main/) repo is for. Using this repo and a simple bash command, we build a Linux OS image containing a specific version of op-rbuilder (identified by its git commit hash), and then calculate the workload ID directly from this Linux OS image. This completes the full chain of trustless verification; given a particular commit hash of flashbots-images (which has hardcoded into it a particular version of op-rbuilder), we can locally build and compute the workload ID, and then pass that to the SDK's `verifyFlashtestationInBlock` function to verify "is Unichain building blocks with the latest version of op-rbuilder?". + +Please see the [flashbots-images](https://github.com/flashbots/flashbots-images/commits/main/) repo instructions on how to calculate a workload ID. + +## API Reference + +### verifyFlashtestationInBlock + +Verify if a block was built by a TEE running a specific workload. + +```typescript +async function verifyFlashtestationInBlock( + workloadIdOrRegisters: string | WorkloadMeasureRegisters, + blockParameter: BlockParameter, + config: ClientConfig +): Promise; +``` + +**Parameters:** + +| Parameter | Type | Description | +| --------------------- | ------------------------------------ | --------------------------------------------------------------------------- | +| workloadIdOrRegisters | `string \| WorkloadMeasureRegisters` | Workload ID (32-byte hex string) or measurement registers to compute the ID | +| blockParameter | `BlockParameter` | Block identifier: tag ('latest', 'earliest', etc.), number, or hash | +| config | `ClientConfig` | Configuration object with `chainId` and optional `rpcUrl` | + +**Returns:** `Promise` + +| Field | Type | Description | +| -------------------- | ---------------- | ------------------------------------------------------------------- | +| isBuiltByExpectedTee | `boolean` | Whether the block was built by the expected TEE workload | +| workloadId | `string \| null` | Workload ID that built the block (null if not TEE-built) | +| commitHash | `string \| null` | Git commit hash of the workload source code (null if not TEE-built) | +| blockExplorerLink | `string \| null` | Block explorer URL (null if not available) | +| builderAddress | `string` | Address of the block builder (optional) | +| version | `number` | Flashtestation protocol version | +| sourceLocators | `string[]` | Source code locations (e.g., GitHub URLs) | + +**Throws:** + +- `NetworkError` - RPC connection failed or network request error +- `BlockNotFoundError` - Block does not exist +- `ValidationError` - Invalid measurement registers +- `ChainNotSupportedError` - Chain ID not supported + +**See [Error Handling](#error-handling) for examples of handling these errors.** + +### Utility Functions + +#### computeWorkloadId + +Compute a workload ID from TEE measurement registers. Useful for debugging or pre-computing IDs. + +```typescript +function computeWorkloadId(registers: WorkloadMeasureRegisters): string; +``` + +Returns the workload ID as a hex string. -To do a one-off build, use `npm run build` or `yarn build`. +#### getSupportedChains -To run tests, use `npm test` or `yarn test`. +Get list of all supported chain IDs. -## Configuration +```typescript +function getSupportedChains(): number[]; +``` -Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. +Returns an array of supported chain IDs: `[130, 1301, 22444422, 33611633]` -### Jest +#### isChainSupported -Jest tests are set up to run with `npm test` or `yarn test`. +Check if a chain ID is supported. -### Bundle Analysis +```typescript +function isChainSupported(chainId: number): boolean; +``` -[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. +Returns `true` if the chain is supported, `false` otherwise. -#### Setup Files +#### getChainConfig -This is the folder structure we set up for you: +Get the full configuration for a chain. -```txt -/src - index.tsx # EDIT THIS -/test - blah.test.tsx # EDIT THIS -.gitignore -package.json -README.md # EDIT THIS -tsconfig.json +```typescript +function getChainConfig(chainId: number): ChainConfig; ``` -### Rollup +Returns a `ChainConfig` object with chain details (name, contract address, RPC URL, block explorer URL). -TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. +**Throws:** `ChainNotSupportedError` if the chain is not supported. -### TypeScript +## Error Handling -`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. +The SDK provides custom error classes for specific failure scenarios. -## Continuous Integration +### NetworkError -### GitHub Actions +Thrown when RPC connection fails or network requests error out. -Two actions are added by default: +```typescript +import { verifyFlashtestationInBlock, NetworkError } from 'flashtestations-sdk'; -- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix -- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 'latest', { + chainId: 1301, + rpcUrl: 'https://invalid-rpc.example.com', + }); +} catch (error) { + if (error instanceof NetworkError) { + console.error('Network error:', error.message); + console.error('Cause:', error.cause); + // Retry with exponential backoff or fallback RPC + } +} +``` -## Optimizations +### BlockNotFoundError -Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: +Thrown when the specified block does not exist on the chain. -```js -// ./types/index.d.ts -declare var __DEV__: boolean; +```typescript +import { BlockNotFoundError } from 'flashtestations-sdk'; + +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 999999999, { + chainId: 1301, + }); +} catch (error) { + if (error instanceof BlockNotFoundError) { + console.error('Block not found:', error.blockParameter); + // Try a different block or handle gracefully + } +} +``` -// inside your code... -if (__DEV__) { - console.log('foo'); +### ValidationError + +Thrown when measurement registers are invalid (wrong format or length). + +```typescript +import { ValidationError } from 'flashtestations-sdk'; + +try { + const invalidRegisters = { + tdAttributes: '0x00', // Too short! + xFAM: '0x0000000000000003', + // ... other fields + }; + const result = await verifyFlashtestationInBlock(invalidRegisters, 'latest', { + chainId: 1301, + }); +} catch (error) { + if (error instanceof ValidationError) { + console.error('Validation error:', error.message); + console.error('Field:', error.field); + // Fix the invalid field + } } ``` -You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. +### ChainNotSupportedError -## Module Formats +Thrown when trying to use an unsupported chain ID. -CJS, ESModules, and UMD module formats are supported. +```typescript +import { ChainNotSupportedError } from 'flashtestations-sdk'; -The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 'latest', { + chainId: 999, // Not supported + }); +} catch (error) { + if (error instanceof ChainNotSupportedError) { + console.error('Chain not supported:', error.chainId); + console.error('Supported chains:', error.supportedChains); + // Use one of the supported chains + } +} +``` + +### Error Handling Best Practices + +- **Retry on NetworkError**: Implement exponential backoff for transient network failures +- **Validate inputs early**: Check chain support with `isChainSupported()` before calling verification +- **Handle missing blocks gracefully**: `BlockNotFoundError` may indicate the block hasn't been mined yet +- **Log error context**: All custom errors include additional context properties for debugging +- **Use fallback RPC endpoints**: Provide alternative `rpcUrl` options for better reliability + +## Advanced Usage + +### Computing Workload IDs + +If you need to compute workload IDs separately (e.g., for caching or debugging), use the `computeWorkloadId` utility: + +```typescript +import { + computeWorkloadId, + WorkloadMeasureRegisters, +} from 'flashtestations-sdk'; + +const registers: WorkloadMeasureRegisters = { + tdAttributes: '0x0000000000000000', + xFAM: '0x0000000000000003', + mrTd: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + mrConfigId: + '0x0000000000000000000000000000000000000000000000000000000000000000', + rtMr0: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + rtMr1: '0xef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abab', + rtMr2: '0x234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeff', + rtMr3: '0x67890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234d', +}; + +const workloadId = computeWorkloadId(registers); +console.log('Computed workload ID:', workloadId); + +// Use the computed ID for verification +const result = await verifyFlashtestationInBlock(workloadId, 'latest', { + chainId: 1301, +}); +``` -## Named Exports +The `computeWorkloadId` function implements the same keccak256 hashing algorithm used on-chain, ensuring consistency between off-chain computation and on-chain verification. -Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. +## Examples -## Including Styles +See the [examples/](./examples) directory for complete runnable examples: -There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. +- `verifyBlock.ts` - Verify blocks with workload ID +- `getFlashtestationTx.ts` - Retrieve flashtestation transaction data -For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. +**Running examples:** -## Publishing to NPM +```bash +# Set your workload ID +export WORKLOAD_ID=0x1234567890abcdef... + +# Run the verification example +npx tsx examples/verifyBlock.ts +``` -We recommend using [np](https://github.com/sindresorhus/np). +## Development + +### Building the SDK + +```bash +yarn build +``` + +This compiles the TypeScript source to CommonJS, ESM, and TypeScript declaration files in the `dist/` directory. + +### Running Tests + +```bash +yarn test +``` + +### Linting + +```bash +yarn lint +``` diff --git a/sdks/flashtestations-sdk/examples/getBlock.ts b/sdks/flashtestations-sdk/examples/getBlock.ts deleted file mode 100644 index 9b24fd877..000000000 --- a/sdks/flashtestations-sdk/examples/getBlock.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createRpcClient } from '../src/rpc/client'; - -/** - * Example: Fetch the most recent block from Unichain Sepolia - * - * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Fetch the latest block using the 'latest' block tag - * 3. Display block information - */ -async function main() { - try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ - chainId: 1301, // Unichain Sepolia testnet - // Optional: provide custom RPC URL - // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, - }); - - console.log('Fetching latest block from Unichain Sepolia...\n'); - - // Fetch the latest block - const block = await client.getBlock('latest'); - - // Display block information - console.log('Block Information:'); - console.log('=================='); - console.log(`Block Number: ${block.number}`); - console.log(`Block Hash: ${block.hash}`); - console.log(`Parent Hash: ${block.parentHash}`); - console.log(`Timestamp: ${block.timestamp} (${new Date(Number(block.timestamp) * 1000).toISOString()})`); - console.log(`Gas Used: ${block.gasUsed}`); - console.log(`Gas Limit: ${block.gasLimit}`); - console.log(`Base Fee Per Gas: ${block.baseFeePerGas ?? 'N/A'}`); - console.log(`Transactions: ${block.transactions.length}`); - console.log(`Miner/Validator: ${block.miner}`); - - // You can also fetch specific blocks: - // - By block number: await client.getBlock(12345) - // - By bigint number: await client.getBlock(BigInt(12345)) - // - By block hash: await client.getBlock('0x...') - // - By hex number: await client.getBlock('0x3039') - // - Other tags: 'earliest', 'finalized', 'safe', 'pending' - - } catch (error) { - console.error('Error fetching block:', error); - process.exit(1); - } -} - -// Run the example -main(); diff --git a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts index 7251b7759..e5bc8e5da 100644 --- a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts +++ b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts @@ -1,29 +1,22 @@ -import { createRpcClient } from '../src/rpc/client'; +import { getFlashtestationTx } from '../src/index'; /** - * Example: Check if a transaction is a flashtestation transaction + * Example: Check if a block contains a flashtestation transaction * * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Check if a transaction emitted the BlockBuilderProofVerified event - * 3. Retrieve the full transaction data if it's a flashtestation transaction - * 4. Handle the case where the transaction is not a flashtestation + * 1. Use the getFlashtestationTx function to fetch flashtestation data from a block + * 2. Retrieve the full flashtestation event data if present + * 3. Handle the case where the block does not contain a flashtestation transaction */ async function main() { try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ + // Fetch flashtestation transaction from the latest block on Unichain Sepolia + const tx = await getFlashtestationTx('latest', { chainId: 1301, // Unichain Sepolia testnet // Optional: provide custom RPC URL // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, }); - // Check if this transaction is a flashtestation - const tx = await client.getFlashtestationTx('latest'); - if (tx) { // This is a flashtestation transaction console.log('\n✓ This is a flashtestation transaction!\n'); @@ -37,10 +30,8 @@ async function main() { 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.'); - console.log('\nThe transaction either:'); - console.log(' 1. Does not exist'); - console.log(' 2. Did not emit the BlockBuilderProofVerified event'); + console.log('\n✗ This is not a flashtestation transaction'); + console.log('\nThe transaction did not emit the BlockBuilderProofVerified event'); } } catch (error) { diff --git a/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts b/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts deleted file mode 100644 index 1b8a2f1de..000000000 --- a/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createRpcClient } from '../src/rpc/client'; - -/** - * Example: Fetch a transaction receipt from Unichain Sepolia - * - * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Fetch a transaction receipt by transaction hash - * 3. Display transaction receipt information - */ -async function main() { - try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ - chainId: 1301, // Unichain Sepolia testnet - // Optional: provide custom RPC URL - // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, - }); - - // fetch an arbitrary transaction hash from the latest block - const txHash = await client.getBlock('latest').then(block => block.transactions[0]) as `0x${string}`; - - // Fetch the transaction receipt - const receipt = await client.getTransactionReceipt(txHash as `0x${string}`); - - // Display transaction receipt information - console.log('Transaction Receipt:'); - console.log('===================='); - console.log(`Transaction Hash: ${receipt.transactionHash}`); - console.log(`Block Number: ${receipt.blockNumber}`); - console.log(`Block Hash: ${receipt.blockHash}`); - console.log(`From: ${receipt.from}`); - console.log(`To: ${receipt.to ?? 'Contract Creation'}`); - console.log(`Contract Address: ${receipt.contractAddress ?? 'N/A'}`); - console.log(`Status: ${receipt.status === 'success' ? '✓ Success' : '✗ Failed'}`); - console.log(`Gas Used: ${receipt.gasUsed}`); - console.log(`Effective Gas Price: ${receipt.effectiveGasPrice}`); - console.log(`Cumulative Gas Used: ${receipt.cumulativeGasUsed}`); - console.log(`Transaction Index: ${receipt.transactionIndex}`); - console.log(`Logs: ${receipt.logs.length} log(s)`); - - // Display logs if present - if (receipt.logs.length > 0) { - console.log('\nLog Details:'); - receipt.logs.forEach((log, index) => { - console.log(`\n Log ${index}:`); - console.log(` Address: ${log.address}`); - console.log(` Topics: ${log.topics.length}`); - console.log(` Data: ${log.data.slice(0, 66)}${log.data.length > 66 ? '...' : ''}`); - }); - } - - // Display bloom filter info - console.log(`\nLogs Bloom: ${receipt.logsBloom.slice(0, 66)}...`); - - } catch (error) { - console.error('Error fetching transaction receipt:', error); - process.exit(1); - } -} - -// Run the example -main(); diff --git a/sdks/flashtestations-sdk/examples/verifyBlock.ts b/sdks/flashtestations-sdk/examples/verifyBlock.ts index 7eb247703..db4e06b06 100644 --- a/sdks/flashtestations-sdk/examples/verifyBlock.ts +++ b/sdks/flashtestations-sdk/examples/verifyBlock.ts @@ -1,4 +1,4 @@ -import { verifyFlashtestationInBlock } from '../src/verification/service'; +import { verifyFlashtestationInBlock } from '../src/index'; /** * Example: Verify if a block was built by a specific TEE workload @@ -43,19 +43,27 @@ async function main() { if (result.isBuiltByExpectedTee) { console.log('\n✓ Block was built by the specified TEE workload!\n'); - console.log(`Workload ID: ${result.workloadId}`); - 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'}`) + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log(`Source Locators: ${result.workloadMetadata.sourceLocators && result.workloadMetadata.sourceLocators.length > 0 ? result.workloadMetadata.sourceLocators.join(', ') : 'None'}`) if (result.blockExplorerLink) { console.log(`Block Explorer: ${result.blockExplorerLink}`); } } else { - console.log('\n✗ Block was NOT built by the specified TEE workload.\n'); - console.log('The block either:'); - console.log(' 1. Does not contain a flashtestation transaction'); - console.log(' 2. Contains a flashtestation from a different workload'); + console.log('\n✗ Block was NOT built by the specified TEE workload\n'); + + if (result.workloadMetadata) { + console.log('Block was built by a different TEE workload:'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log(`Source Locators: ${result.workloadMetadata.sourceLocators.length > 0 ? result.workloadMetadata.sourceLocators.join(', ') : 'None'}`) + } else { + console.log('The block does not contain a flashtestation transaction') + } } // You can also verify specific blocks: diff --git a/sdks/flashtestations-sdk/package.json b/sdks/flashtestations-sdk/package.json index 7061d2513..1aa38cb1e 100644 --- a/sdks/flashtestations-sdk/package.json +++ b/sdks/flashtestations-sdk/package.json @@ -6,7 +6,8 @@ "keywords": [ "flashteststations", "ethereum", - "flashbots" + "flashbots", + "unichain" ], "version": "0.1.0", "license": "MIT", diff --git a/sdks/flashtestations-sdk/src/index.ts b/sdks/flashtestations-sdk/src/index.ts index 11168379c..ec86a66b2 100644 --- a/sdks/flashtestations-sdk/src/index.ts +++ b/sdks/flashtestations-sdk/src/index.ts @@ -1,7 +1,38 @@ -// TODO: Remove this -export const sum = (a: number, b: number) => { - if ('development' === process.env.NODE_ENV) { - console.log('boop'); - } - return a + b; -}; +/** + * Flashtestations SDK - Verify TEE-built blocks on Unichain + * + * This SDK provides tools to verify whether blockchain blocks were built by + * Trusted Execution Environments (TEEs) running specific workload software. + */ + +// Main verification function +export { verifyFlashtestationInBlock, getFlashtestationTx } from './verification/service'; + + +// Core types +export type { + VerificationResult, + WorkloadMeasureRegisters, + BlockParameter, + FlashtestationEvent, + ChainConfig, + ClientConfig, +} from './types'; + +// Error classes for programmatic error handling +export { + NetworkError, + BlockNotFoundError, + ValidationError, + ChainNotSupportedError, +} from './types'; + +// Chain configuration utilities +export { + getSupportedChains, + isChainSupported, + getChainConfig, +} from './config/chains'; + +// Workload ID computation utility +export { computeWorkloadId } from './crypto/workload'; diff --git a/sdks/flashtestations-sdk/src/types/index.ts b/sdks/flashtestations-sdk/src/types/index.ts index dfa6686fd..6bdf95e7e 100644 --- a/sdks/flashtestations-sdk/src/types/index.ts +++ b/sdks/flashtestations-sdk/src/types/index.ts @@ -1,23 +1,32 @@ -/** - * Result of flashtestation verification - */ -export interface VerificationResult { - /** Whether the block was built by a TEE running the specified workload */ - isBuiltByExpectedTee: boolean; - /** workload ID of the TEE workload, null if not TEE-built */ - workloadId: string | null; - /** Commit hash of the TEE workload source code, null if not TEE-built */ - commitHash: string | null; - /** Block explorer link for the block, null if not TEE-built */ - blockExplorerLink: string | null; +export type WorkloadMetadata = { + /** workload ID of the TEE workload*/ + workloadId: string; + /** Commit hash of the TEE workload source code */ + commitHash: string; /** Address of the block builder, optional */ - builderAddress?: string; + 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[]; + sourceLocators: string[]; } +/** + * Result of flashtestation verification + */ +export type VerificationResult = | { + /** Block was built by the expected TEE workload */ + isBuiltByExpectedTee: true; + blockExplorerLink: string | null; + workloadMetadata: WorkloadMetadata; +} +| { + /** Block was NOT built by the expected TEE workload */ + isBuiltByExpectedTee: false; + blockExplorerLink: string | null; + workloadMetadata: WorkloadMetadata | null; +}; + /** * TEE workload measurement registers used for workload ID computation */ @@ -74,6 +83,16 @@ export interface ChainConfig { blockExplorerUrl: string; } +/** + * Minimal configuration options for the JSON-RPC client to interact with the blockchain + */ +export interface ClientConfig { + /** Chain ID to network */ + chainId: number; + /** Optional custom RPC URL (overrides default) */ + rpcUrl?: string; +} + /** * Block parameter for identifying blocks */ diff --git a/sdks/flashtestations-sdk/src/verification/service.ts b/sdks/flashtestations-sdk/src/verification/service.ts index 8d61f4973..cde170662 100644 --- a/sdks/flashtestations-sdk/src/verification/service.ts +++ b/sdks/flashtestations-sdk/src/verification/service.ts @@ -5,16 +5,47 @@ import { BlockParameter, VerificationResult, WorkloadMeasureRegisters, + ClientConfig, + FlashtestationEvent, } from '../types'; /** - * Configuration options for verification + * Fetch the flashtestation transaction from a specific block + * + * This function retrieves the flashtestation transaction (if any) from the specified block. + * Unlike verifyFlashtestationInBlock, this does not perform any workload verification - it + * simply returns the raw flashtestation event data. + * + * @param blockParameter - Block identifier (tag, number, or hash), defaults to 'latest' + * @param config - Configuration for chain and RPC connection + * @returns FlashtestationEvent if the block contains a flashtestation transaction, null otherwise + * @throws NetworkError if RPC connection fails + * @throws BlockNotFoundError if block doesn't exist + * + * @example + * // Get flashtestation transaction from the latest block + * const flashtestation = await getFlashtestationTx('latest', { chainId: 1301 }); + * if (flashtestation) { + * console.log('Workload ID:', flashtestation.workloadId); + * console.log('Commit Hash:', flashtestation.commitHash); + * } + * + * @example + * // Get flashtestation transaction from a specific block number + * const flashtestation = await getFlashtestationTx(12345, { chainId: 1301 }); */ -export interface VerificationConfig { - /** Chain ID to verify on */ - chainId: number; - /** Optional custom RPC URL (overrides default) */ - rpcUrl?: string; +export async function getFlashtestationTx( + blockParameter: BlockParameter = 'latest', + config: ClientConfig +): Promise { + // Create RPC client + const client = new RpcClient({ + chainId: config.chainId, + rpcUrl: config.rpcUrl, + }); + + // Get the flashtestation transaction from the block + return await client.getFlashtestationTx(blockParameter); } /** @@ -61,7 +92,7 @@ export interface VerificationConfig { export async function verifyFlashtestationInBlock( workloadIdOrRegisters: string | WorkloadMeasureRegisters, blockParameter: BlockParameter, - config: VerificationConfig + config: ClientConfig ): Promise { // Determine if we need to compute workload ID from registers let workloadId: string; @@ -93,27 +124,8 @@ export async function verifyFlashtestationInBlock( if (!flashtestationEvent) { return { isBuiltByExpectedTee: false, - workloadId: null, - commitHash: null, blockExplorerLink: null, - version: 0, - }; - } - - // Normalize event workload ID for comparison - const eventWorkloadId = flashtestationEvent.workloadId.toLowerCase(); - - // Compare workload IDs (byte-wise comparison) - const workloadMatches = workloadId === eventWorkloadId; - - if (!workloadMatches) { - // Block was built by a TEE, but not the one we're looking for - return { - isBuiltByExpectedTee: false, - workloadId: null, - commitHash: null, - blockExplorerLink: null, - version: 0, + workloadMetadata: null, }; } @@ -130,14 +142,37 @@ export async function verifyFlashtestationInBlock( blockExplorerLink = `${blockExplorerBaseUrl}/block/${block.number}`; } + // Normalize event workload ID for comparison + const eventWorkloadId = flashtestationEvent.workloadId.toLowerCase(); + + // Compare workload IDs (byte-wise comparison) + const workloadMatches = workloadId === eventWorkloadId; + + if (!workloadMatches) { + // Block was built by a TEE, but not the one we're looking for + return { + isBuiltByExpectedTee: false, + blockExplorerLink: blockExplorerLink, + workloadMetadata: { + workloadId: flashtestationEvent.workloadId, + commitHash: flashtestationEvent.commitHash, + builderAddress: flashtestationEvent.caller, + version: flashtestationEvent.version, + sourceLocators: flashtestationEvent.sourceLocators, + } + }; + } + // Block was built by the specified TEE workload return { isBuiltByExpectedTee: true, - workloadId: flashtestationEvent.workloadId, - commitHash: flashtestationEvent.commitHash, blockExplorerLink: blockExplorerLink, - builderAddress: flashtestationEvent.caller, - version: flashtestationEvent.version, - sourceLocators: flashtestationEvent.sourceLocators, + workloadMetadata: { + workloadId: flashtestationEvent.workloadId, + commitHash: flashtestationEvent.commitHash, + builderAddress: flashtestationEvent.caller, + version: flashtestationEvent.version, + sourceLocators: flashtestationEvent.sourceLocators, + } }; } diff --git a/sdks/flashtestations-sdk/test/verification/service.test.ts b/sdks/flashtestations-sdk/test/verification/service.test.ts index 9d1123f9b..00d3002e7 100644 --- a/sdks/flashtestations-sdk/test/verification/service.test.ts +++ b/sdks/flashtestations-sdk/test/verification/service.test.ts @@ -55,6 +55,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -73,11 +74,14 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'abc123def456', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/12345', - builderAddress: '0xbuilder123', - version: 1, - workloadId: workloadId, + workloadMetadata: { + workloadId: workloadId, + commitHash: 'abc123def456', + builderAddress: '0xbuilder123', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); @@ -92,6 +96,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -119,6 +124,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -149,10 +155,8 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, blockExplorerLink: null, - version: 0, - workloadId: null, + workloadMetadata: null, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); @@ -167,9 +171,17 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + + }; + + const mockBlock = { + number: BigInt(12345), + hash: '0xblockhash', }; mockGetFlashtestationTx.mockResolvedValue(mockEvent); + mockGetBlock.mockResolvedValue(mockBlock); const result = await verifyFlashtestationInBlock( workloadId, @@ -179,14 +191,18 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, - blockExplorerLink: null, - version: 0, - workloadId: null, + blockExplorerLink: 'https://sepolia.uniscan.xyz/block/12345', + workloadMetadata: { + workloadId: differentWorkloadId, + commitHash: 'abc123def456', + builderAddress: '0xbuilder123', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); - expect(mockGetBlock).not.toHaveBeenCalled(); + expect(mockGetBlock).toHaveBeenCalledWith('latest'); }); it('should handle null block explorer URL', async () => { @@ -291,6 +307,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -310,11 +327,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(registers); expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'register-commit', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', - builderAddress: '0xbuilder456', - version: 1, - workloadId: computedWorkloadId, + workloadMetadata: { + workloadId: computedWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); }); @@ -336,6 +356,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -355,11 +376,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(uppercaseRegisters); expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'register-commit', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', - builderAddress: '0xbuilder456', - version: 1, - workloadId: computedWorkloadId, + workloadMetadata: { + workloadId: computedWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); }); @@ -371,9 +395,16 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }; + + const mockBlock = { + number: BigInt(99999), + hash: '0xblockhash', }; mockGetFlashtestationTx.mockResolvedValue(mockEvent); + mockGetBlock.mockResolvedValue(mockBlock); const result = await verifyFlashtestationInBlock( registers, @@ -384,10 +415,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(registers); expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, - blockExplorerLink: null, - version: 0, - workloadId: null, + blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', + workloadMetadata: { + workloadId: differentWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); });